Compare commits

..

3 Commits

Author SHA1 Message Date
Claude Bot
acbaf3c752 fix(bundler): normalize paths to posix in FileMap top_level_dir fallback
On Windows, both the specifier and top_level_dir use backslashes, but
FileMap keys use forward slashes. Normalize both to posix before
prefix-matching and map lookup so the fallback works cross-platform.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-02 13:32:25 +00:00
Claude Bot
933d5cbee0 fix(bundler): validate path segment boundary when stripping top_level_dir prefix
Address review feedback: ensure we only strip top_level_dir when it matches
at a path segment boundary (separator character), preventing false positives
like "/work" matching "/workspace/...". Also extract makeResult helper to
reduce duplication.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-02 12:41:26 +00:00
Claude Bot
c00371e2af fix(bundler): resolve virtual files from HTML <script src> with absolute paths
When `Bun.build()` is used with a virtual HTML entrypoint (via `files` map)
that references other virtual files through absolute `<script src="/...">` paths,
the HTMLScanner rewrites those paths by joining them with `top_level_dir` (CWD).
This causes FileMap lookups to fail because the map keys use the original paths.

Add a fallback in `FileMap.resolve` that strips the `top_level_dir` prefix from
absolute specifiers and retries the lookup with the original path.

Closes #27687

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-02 12:41:26 +00:00
10 changed files with 207 additions and 503 deletions

View File

@@ -124,32 +124,22 @@ Return a string to replace the element's rendering. Return `null` or `undefined`
### Block callbacks
| Callback | Meta | Description |
| ------------ | --------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `heading` | `{ level, id? }` | Heading level 16. `id` is set when `headings: { ids: true }` is enabled |
| `paragraph` | — | Paragraph block |
| `blockquote` | — | Blockquote block |
| `code` | `{ language? }` | Fenced or indented code block. `language` is the info-string when specified on the fence |
| `list` | `{ ordered, start?, depth }` | `depth` is nesting level (0 = top-level). `start` is set for ordered lists |
| `listItem` | `{ index, depth, ordered, start?, checked? }` | See [List item meta](#list-item-meta) below |
| `hr` | — | Horizontal rule |
| `table` | — | Table block |
| `thead` | — | Table head |
| `tbody` | — | Table body |
| `tr` | — | Table row |
| `th` | `{ align? }` | Table header cell. `align` is `"left"`, `"center"`, `"right"`, or absent |
| `td` | `{ align? }` | Table data cell |
| `html` | — | Raw HTML content |
#### List item meta
The `listItem` callback receives everything needed to render markers directly:
- `index` — 0-based position within the parent list
- `depth` — the parent list's nesting level (0 = top-level)
- `ordered` — whether the parent list is ordered
- `start` — the parent list's start number (only when `ordered` is true)
- `checked` — task list state (only for `- [x]` / `- [ ]` items)
| Callback | Meta | Description |
| ------------ | ------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `heading` | `{ level: number, id?: string }` | Heading level 16. `id` is set when `headings: { ids: true }` is enabled |
| `paragraph` | — | Paragraph block |
| `blockquote` | — | Blockquote block |
| `code` | `{ language?: string }` | Fenced or indented code block. `language` is the info-string when specified on the fence |
| `list` | `{ ordered: boolean, start?: number }` | Ordered or unordered list. `start` is the start number for ordered lists |
| `listItem` | `{ checked?: boolean }` | List item. `checked` is set for task list items (`- [x]` / `- [ ]`) |
| `hr` | — | Horizontal rule |
| `table` | — | Table block |
| `thead` | — | Table head |
| `tbody` | — | Table body |
| `tr` | — | Table row |
| `th` | `{ align?: "left" \| "center" \| "right" }` | Table header cell. `align` is set when alignment is specified |
| `td` | `{ align?: "left" \| "center" \| "right" }` | Table data cell. `align` is set when alignment is specified |
| `html` | — | Raw HTML content |
### Inline callbacks
@@ -215,33 +205,6 @@ const ansi = Bun.markdown.render("# Hello\n\nThis is **bold** and *italic*", {
});
```
#### Nested list numbering
The `listItem` callback receives everything needed to render markers directly — no post-processing:
```ts
const result = Bun.markdown.render("1. first\n 1. sub-a\n 2. sub-b\n2. second", {
listItem: (children, { index, depth, ordered, start }) => {
const n = (start ?? 1) + index;
// 1. 2. 3. at depth 0, a. b. c. at depth 1, i. ii. iii. at depth 2
const marker = !ordered
? "-"
: depth === 0
? `${n}.`
: depth === 1
? `${String.fromCharCode(96 + n)}.`
: `${toRoman(n)}.`;
return " ".repeat(depth) + marker + " " + children.trimEnd() + "\n";
},
// Prepend a newline so nested lists are separated from their parent item's text
list: children => "\n" + children,
});
// 1. first
// a. sub-a
// b. sub-b
// 2. second
```
#### Code block syntax highlighting
````ts

View File

@@ -1193,20 +1193,10 @@ declare module "bun" {
ordered: boolean;
/** The start number for ordered lists. */
start?: number;
/** Nesting depth. `0` for a top-level list, `1` for a list inside a list item, etc. */
depth: number;
}
/** Meta passed to the `listItem` callback. */
interface ListItemMeta {
/** 0-based index of this item within its parent list. */
index: number;
/** Nesting depth of the parent list. `0` for items in a top-level list. */
depth: number;
/** Whether the parent list is ordered. */
ordered: boolean;
/** The start number of the parent list (only set when `ordered` is true). */
start?: number;
/** Task list checked state. Set for `- [x]` / `- [ ]` items. */
checked?: boolean;
}
@@ -1244,8 +1234,8 @@ declare module "bun" {
code?: (children: string, meta?: CodeBlockMeta) => string | null | undefined;
/** Ordered or unordered list. `start` is the first item number for ordered lists. */
list?: (children: string, meta: ListMeta) => string | null | undefined;
/** List item. `meta` always includes `{index, depth, ordered}`. `meta.start` is set for ordered lists; `meta.checked` is set for task list items. */
listItem?: (children: string, meta: ListItemMeta) => string | null | undefined;
/** List item. `meta.checked` is set for task list items (`- [x]` / `- [ ]`). Only passed for task list items. */
listItem?: (children: string, meta?: ListItemMeta) => string | null | undefined;
/** Horizontal rule. */
hr?: (children: string) => string | null | undefined;
/** Table. */

View File

@@ -66,12 +66,7 @@ pub const JSBundler = struct {
// Must use getKey to return the map's owned key, not the parameter
if (comptime !bun.Environment.isWindows) {
if (self.map.getKey(specifier)) |key| {
return _resolver.Result{
.path_pair = .{
.primary = Fs.Path.initWithNamespace(key, "file"),
},
.module_type = .unknown,
};
return makeResult(key);
}
} else {
const buf = bun.path_buffer_pool.get();
@@ -79,12 +74,55 @@ pub const JSBundler = struct {
const normalized_specifier = bun.path.pathToPosixBuf(u8, specifier, buf);
if (self.map.getKey(normalized_specifier)) |key| {
return _resolver.Result{
.path_pair = .{
.primary = Fs.Path.initWithNamespace(key, "file"),
},
.module_type = .unknown,
};
return makeResult(key);
}
}
// For absolute specifiers, check if the specifier was produced by the HTML
// scanner's top_level_dir join (e.g., "/virtual/foo.tsx" -> "<CWD>/virtual/foo.tsx").
// If so, try stripping the top_level_dir prefix and looking up the original path.
//
// On Windows, both the specifier and top_level_dir may use backslashes, so we
// normalize both to posix before prefix-matching and map lookup.
if (specifier.len > 0 and isAbsolutePath(specifier)) {
const top_level_dir = Fs.FileSystem.instance.top_level_dir;
if (top_level_dir.len > 0) {
// Normalize both specifier and top_level_dir to posix for consistent matching.
const norm_spec_buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(norm_spec_buf);
const norm_tld_buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(norm_tld_buf);
const norm_specifier = bun.path.pathToPosixBuf(u8, specifier, norm_spec_buf);
const norm_top_level_dir = bun.path.pathToPosixBuf(u8, top_level_dir, norm_tld_buf);
if (bun.strings.hasPrefix(norm_specifier, norm_top_level_dir)) {
const after_prefix = norm_specifier[norm_top_level_dir.len..];
if (after_prefix.len == 0) {
// specifier == top_level_dir exactly, nothing to look up
} else if (after_prefix[0] == '/') {
// top_level_dir ended without separator, remainder starts with one.
// e.g. top_level_dir="/workspace/bun", specifier="/workspace/bun/virtual/foo.tsx"
// -> after_prefix="/virtual/foo.tsx" (already a valid absolute path)
if (self.map.getKey(after_prefix)) |key| {
return makeResult(key);
}
} else if (norm_top_level_dir[norm_top_level_dir.len - 1] == '/') {
// top_level_dir ended with separator (e.g. "/workspace/bun/"),
// so after_prefix is "virtual/foo.tsx" — prepend "/" to reconstruct
// the original absolute path "/virtual/foo.tsx".
const orig_buf = bun.path_buffer_pool.get();
defer bun.path_buffer_pool.put(orig_buf);
orig_buf[0] = '/';
@memcpy(orig_buf[1..][0..after_prefix.len], after_prefix);
const original_path = orig_buf[0 .. after_prefix.len + 1];
if (self.map.getKey(original_path)) |key| {
return makeResult(key);
}
}
// else: partial segment match (e.g. "/work" matched "/workspace/..."),
// not a valid top_level_dir prefix — skip this fallback.
}
}
}
@@ -133,12 +171,7 @@ pub const JSBundler = struct {
const joined = buf[0..joined_len];
// Must use getKey to return the map's owned key, not the temporary buffer
if (self.map.getKey(joined)) |key| {
return _resolver.Result{
.path_pair = .{
.primary = Fs.Path.initWithNamespace(key, "file"),
},
.module_type = .unknown,
};
return makeResult(key);
}
}
@@ -162,6 +195,15 @@ pub const JSBundler = struct {
return false;
}
fn makeResult(key: []const u8) _resolver.Result {
return _resolver.Result{
.path_pair = .{
.primary = Fs.Path.initWithNamespace(key, "file"),
},
.module_type = .unknown,
};
}
/// Parse the files option from JavaScript.
/// Expected format: Record<string, string | Blob | File | TypedArray | ArrayBuffer>
/// Uses async parsing for cross-thread safety since bundler runs on a separate thread.

View File

@@ -759,12 +759,8 @@ const JsCallbackRenderer = struct {
const StackEntry = struct {
buffer: std.ArrayListUnmanaged(u8) = .{},
block_type: md.BlockType = .doc,
data: u32 = 0,
flags: u32 = 0,
/// For ul/ol: number of li children seen so far (next li's index).
/// For li: this item's 0-based index within its parent list.
child_index: u32 = 0,
detail: md.SpanDetail = .{},
};
@@ -857,22 +853,7 @@ const JsCallbackRenderer = struct {
if (block_type == .h) {
self.#heading_tracker.enterHeading();
}
// For li: record its 0-based index within the parent list, then
// increment the parent's counter so the next sibling gets index+1.
var child_index: u32 = 0;
if (block_type == .li and self.#stack.items.len > 0) {
const parent = &self.#stack.items[self.#stack.items.len - 1];
child_index = parent.child_index;
parent.child_index += 1;
}
try self.#stack.append(self.#allocator, .{
.block_type = block_type,
.data = data,
.flags = flags,
.child_index = child_index,
});
try self.#stack.append(self.#allocator, .{ .data = data, .flags = flags });
}
fn leaveBlockImpl(ptr: *anyopaque, block_type: md.BlockType, _: u32) bun.JSError!void {
@@ -1005,30 +986,6 @@ const JsCallbackRenderer = struct {
// Metadata object creation
// ========================================
/// Walks the stack to count enclosing ul/ol blocks. Called during leave,
/// so the top entry is the block itself (skip it for li, count it for ul/ol's
/// own depth which excludes self).
fn countListDepth(self: *JsCallbackRenderer) u32 {
var depth: u32 = 0;
// Skip the top entry (self) — we want enclosing lists only.
const len = self.#stack.items.len;
if (len < 2) return 0;
for (self.#stack.items[0 .. len - 1]) |entry| {
if (entry.block_type == .ul or entry.block_type == .ol) depth += 1;
}
return depth;
}
/// Returns the parent ul/ol entry for the current li (top of stack).
/// Returns null if the stack shape is unexpected.
fn parentList(self: *JsCallbackRenderer) ?*const StackEntry {
const len = self.#stack.items.len;
if (len < 2) return null;
const parent = &self.#stack.items[len - 2];
if (parent.block_type == .ul or parent.block_type == .ol) return parent;
return null;
}
fn createBlockMeta(self: *JsCallbackRenderer, block_type: md.BlockType, data: u32, flags: u32) bun.JSError!?JSValue {
const g = self.#globalObject;
switch (block_type) {
@@ -1043,10 +1000,15 @@ const JsCallbackRenderer = struct {
return obj;
},
.ol => {
return BunMarkdownMeta__createList(g, true, JSValue.jsNumber(data), self.countListDepth());
const obj = JSValue.createEmptyObject(g, 2);
obj.put(g, ZigString.static("ordered"), .true);
obj.put(g, ZigString.static("start"), JSValue.jsNumber(data));
return obj;
},
.ul => {
return BunMarkdownMeta__createList(g, false, .js_undefined, self.countListDepth());
const obj = JSValue.createEmptyObject(g, 1);
obj.put(g, ZigString.static("ordered"), .false);
return obj;
},
.code => {
if (flags & md.BLOCK_FENCED_CODE != 0) {
@@ -1061,31 +1023,21 @@ const JsCallbackRenderer = struct {
},
.th, .td => {
const alignment = md.types.alignmentFromData(data);
const align_js = if (md.types.alignmentName(alignment)) |align_str|
try bun.String.createUTF8ForJS(g, align_str)
else
JSValue.js_undefined;
return BunMarkdownMeta__createCell(g, align_js);
if (md.types.alignmentName(alignment)) |align_str| {
const obj = JSValue.createEmptyObject(g, 1);
obj.put(g, ZigString.static("align"), try bun.String.createUTF8ForJS(g, align_str));
return obj;
}
return null;
},
.li => {
// The li entry is still on top of the stack; parent ul/ol is at len-2.
const len = self.#stack.items.len;
const item_index = if (len > 1) self.#stack.items[len - 1].child_index else 0;
const parent = self.parentList();
const is_ordered = parent != null and parent.?.block_type == .ol;
// countListDepth() includes the immediate parent list; subtract it
// so that items in a top-level list report depth 0.
const enclosing = self.countListDepth();
const depth: u32 = if (enclosing > 0) enclosing - 1 else 0;
const task_mark = md.types.taskMarkFromData(data);
const start_js = if (is_ordered) JSValue.jsNumber(parent.?.data) else JSValue.js_undefined;
const checked_js = if (task_mark != 0)
JSValue.jsBoolean(md.types.isTaskChecked(task_mark))
else
JSValue.js_undefined;
return BunMarkdownMeta__createListItem(g, item_index, depth, is_ordered, start_js, checked_js);
if (task_mark != 0) {
const obj = JSValue.createEmptyObject(g, 1);
obj.put(g, ZigString.static("checked"), JSValue.jsBoolean(md.types.isTaskChecked(task_mark)));
return obj;
}
return null;
},
else => return null,
}
@@ -1095,18 +1047,14 @@ const JsCallbackRenderer = struct {
const g = self.#globalObject;
switch (span_type) {
.a => {
const href = try bun.String.createUTF8ForJS(g, detail.href);
const title = if (detail.title.len > 0)
try bun.String.createUTF8ForJS(g, detail.title)
else
JSValue.js_undefined;
return BunMarkdownMeta__createLink(g, href, title);
const obj = JSValue.createEmptyObject(g, 2);
obj.put(g, ZigString.static("href"), try bun.String.createUTF8ForJS(g, detail.href));
if (detail.title.len > 0) {
obj.put(g, ZigString.static("title"), try bun.String.createUTF8ForJS(g, detail.title));
}
return obj;
},
.img => {
// Image meta shares shape with link (src/href are both the first
// field). We use a separate cached structure would require a
// second slot, so just fall back to the generic path here —
// images are rare enough that it doesn't matter.
const obj = JSValue.createEmptyObject(g, 2);
obj.put(g, ZigString.static("src"), try bun.String.createUTF8ForJS(g, detail.href));
if (detail.title.len > 0) {
@@ -1166,14 +1114,6 @@ const TagIndex = enum(u8) {
extern fn BunMarkdownTagStrings__getTagString(*jsc.JSGlobalObject, u8) JSValue;
// Fast-path meta-object constructors using cached Structures (see
// BunMarkdownMeta.cpp). Each constructs via putDirectOffset so the
// resulting objects share a single Structure and stay monomorphic.
extern fn BunMarkdownMeta__createListItem(*jsc.JSGlobalObject, u32, u32, bool, JSValue, JSValue) JSValue;
extern fn BunMarkdownMeta__createList(*jsc.JSGlobalObject, bool, JSValue, u32) JSValue;
extern fn BunMarkdownMeta__createCell(*jsc.JSGlobalObject, JSValue) JSValue;
extern fn BunMarkdownMeta__createLink(*jsc.JSGlobalObject, JSValue, JSValue) JSValue;
fn getCachedTagString(globalObject: *jsc.JSGlobalObject, tag: TagIndex) JSValue {
return BunMarkdownTagStrings__getTagString(globalObject, @intFromEnum(tag));
}

View File

@@ -1,123 +0,0 @@
#include "BunMarkdownMeta.h"
#include "JavaScriptCore/JSObjectInlines.h"
#include "JavaScriptCore/ObjectConstructor.h"
#include "JavaScriptCore/JSCast.h"
using namespace JSC;
namespace Bun {
namespace MarkdownMeta {
// Builds a cached Structure with N fixed property offsets. Properties are
// laid out in declaration order so the extern "C" create functions can use
// putDirectOffset without name lookups.
static Structure* buildStructure(VM& vm, JSGlobalObject* globalObject, std::initializer_list<ASCIILiteral> names)
{
Structure* structure = globalObject->structureCache().emptyObjectStructureForPrototype(
globalObject,
globalObject->objectPrototype(),
names.size());
PropertyOffset offset;
PropertyOffset expected = 0;
for (auto name : names) {
structure = structure->addPropertyTransition(vm, structure, Identifier::fromString(vm, name), 0, offset);
ASSERT_UNUSED(expected, offset == expected);
expected++;
}
return structure;
}
Structure* createListItemMetaStructure(VM& vm, JSGlobalObject* globalObject)
{
return buildStructure(vm, globalObject, { "index"_s, "depth"_s, "ordered"_s, "start"_s, "checked"_s });
}
Structure* createListMetaStructure(VM& vm, JSGlobalObject* globalObject)
{
return buildStructure(vm, globalObject, { "ordered"_s, "start"_s, "depth"_s });
}
Structure* createCellMetaStructure(VM& vm, JSGlobalObject* globalObject)
{
return buildStructure(vm, globalObject, { "align"_s });
}
Structure* createLinkMetaStructure(VM& vm, JSGlobalObject* globalObject)
{
return buildStructure(vm, globalObject, { "href"_s, "title"_s });
}
} // namespace MarkdownMeta
} // namespace Bun
// ──────────────────────────────────────────────────────────────────────────
// extern "C" constructors — callable from MarkdownObject.zig
// ──────────────────────────────────────────────────────────────────────────
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createListItem(
JSGlobalObject* globalObject,
uint32_t index,
uint32_t depth,
bool ordered,
EncodedJSValue start,
EncodedJSValue checked)
{
auto* global = jsCast<Zig::GlobalObject*>(globalObject);
VM& vm = global->vm();
JSObject* obj = constructEmptyObject(vm, global->JSMarkdownListItemMetaStructure());
obj->putDirectOffset(vm, 0, jsNumber(index));
obj->putDirectOffset(vm, 1, jsNumber(depth));
obj->putDirectOffset(vm, 2, jsBoolean(ordered));
obj->putDirectOffset(vm, 3, JSValue::decode(start));
obj->putDirectOffset(vm, 4, JSValue::decode(checked));
return JSValue::encode(obj);
}
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createList(
JSGlobalObject* globalObject,
bool ordered,
EncodedJSValue start,
uint32_t depth)
{
auto* global = jsCast<Zig::GlobalObject*>(globalObject);
VM& vm = global->vm();
JSObject* obj = constructEmptyObject(vm, global->JSMarkdownListMetaStructure());
obj->putDirectOffset(vm, 0, jsBoolean(ordered));
obj->putDirectOffset(vm, 1, JSValue::decode(start));
obj->putDirectOffset(vm, 2, jsNumber(depth));
return JSValue::encode(obj);
}
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createCell(
JSGlobalObject* globalObject,
EncodedJSValue align)
{
auto* global = jsCast<Zig::GlobalObject*>(globalObject);
VM& vm = global->vm();
JSObject* obj = constructEmptyObject(vm, global->JSMarkdownCellMetaStructure());
obj->putDirectOffset(vm, 0, JSValue::decode(align));
return JSValue::encode(obj);
}
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createLink(
JSGlobalObject* globalObject,
EncodedJSValue href,
EncodedJSValue title)
{
auto* global = jsCast<Zig::GlobalObject*>(globalObject);
VM& vm = global->vm();
JSObject* obj = constructEmptyObject(vm, global->JSMarkdownLinkMetaStructure());
obj->putDirectOffset(vm, 0, JSValue::decode(href));
obj->putDirectOffset(vm, 1, JSValue::decode(title));
return JSValue::encode(obj);
}

View File

@@ -1,61 +0,0 @@
#pragma once
#include "root.h"
#include "headers.h"
#include "JavaScriptCore/JSObjectInlines.h"
#include "ZigGlobalObject.h"
using namespace JSC;
namespace Bun {
namespace MarkdownMeta {
// Cached Structures for the small metadata objects passed as the second
// argument to Bun.markdown.render() callbacks. These have fixed shapes
// so JSC's property access inline caches stay monomorphic and we avoid
// the string-hash + property-transition cost of `put()`-style construction
// on every callback (which matters a lot for list items and table cells).
Structure* createListItemMetaStructure(VM& vm, JSGlobalObject* globalObject);
Structure* createListMetaStructure(VM& vm, JSGlobalObject* globalObject);
Structure* createCellMetaStructure(VM& vm, JSGlobalObject* globalObject);
Structure* createLinkMetaStructure(VM& vm, JSGlobalObject* globalObject);
} // namespace MarkdownMeta
} // namespace Bun
// ListItemMeta: {index, depth, ordered, start, checked}
// `start` and `checked` are always present (jsUndefined() when not applicable)
// so the shape is fixed.
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createListItem(
JSGlobalObject* globalObject,
uint32_t index,
uint32_t depth,
bool ordered,
EncodedJSValue start, // jsNumber or jsUndefined
EncodedJSValue checked // jsBoolean or jsUndefined
);
// ListMeta: {ordered, start, depth}
// `start` is always present (jsUndefined for unordered).
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createList(
JSGlobalObject* globalObject,
bool ordered,
EncodedJSValue start, // jsNumber or jsUndefined
uint32_t depth);
// CellMeta: {align}
// `align` is always present (jsUndefined when no alignment).
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createCell(
JSGlobalObject* globalObject,
EncodedJSValue align // jsString or jsUndefined
);
// LinkMeta / ImageMeta: {href, title} or {src, title}
// `title` is always present (jsUndefined when missing). `href` and `src`
// share the structure slot (first property) — the property name differs
// but the shape is the same; two separate structures are used.
extern "C" JSC::EncodedJSValue BunMarkdownMeta__createLink(
JSGlobalObject* globalObject,
EncodedJSValue href,
EncodedJSValue title // jsString or jsUndefined
);

View File

@@ -124,7 +124,6 @@
#include "JSSink.h"
#include "JSSocketAddressDTO.h"
#include "JSReactElement.h"
#include "BunMarkdownMeta.h"
#include "JSSQLStatement.h"
#include "JSStringDecoder.h"
#include "JSTextEncoder.h"
@@ -1803,23 +1802,6 @@ void GlobalObject::finishCreation(VM& vm)
init.set(Bun::JSReactElement::createStructure(init.vm, init.owner));
});
m_JSMarkdownListItemMetaStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(Bun::MarkdownMeta::createListItemMetaStructure(init.vm, init.owner));
});
m_JSMarkdownListMetaStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(Bun::MarkdownMeta::createListMetaStructure(init.vm, init.owner));
});
m_JSMarkdownCellMetaStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(Bun::MarkdownMeta::createCellMetaStructure(init.vm, init.owner));
});
m_JSMarkdownLinkMetaStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(Bun::MarkdownMeta::createLinkMetaStructure(init.vm, init.owner));
});
m_JSSQLStatementStructure.initLater(
[](const Initializer<Structure>& init) {
init.set(WebCore::createJSSQLStatementStructure(init.owner));

View File

@@ -302,10 +302,6 @@ public:
Structure* CommonJSModuleObjectStructure() const { return m_commonJSModuleObjectStructure.getInitializedOnMainThread(this); }
Structure* JSSocketAddressDTOStructure() const { return m_JSSocketAddressDTOStructure.getInitializedOnMainThread(this); }
Structure* JSReactElementStructure() const { return m_JSReactElementStructure.getInitializedOnMainThread(this); }
Structure* JSMarkdownListItemMetaStructure() const { return m_JSMarkdownListItemMetaStructure.getInitializedOnMainThread(this); }
Structure* JSMarkdownListMetaStructure() const { return m_JSMarkdownListMetaStructure.getInitializedOnMainThread(this); }
Structure* JSMarkdownCellMetaStructure() const { return m_JSMarkdownCellMetaStructure.getInitializedOnMainThread(this); }
Structure* JSMarkdownLinkMetaStructure() const { return m_JSMarkdownLinkMetaStructure.getInitializedOnMainThread(this); }
Structure* ImportMetaObjectStructure() const { return m_importMetaObjectStructure.getInitializedOnMainThread(this); }
Structure* ImportMetaBakeObjectStructure() const { return m_importMetaBakeObjectStructure.getInitializedOnMainThread(this); }
Structure* AsyncContextFrameStructure() const { return m_asyncBoundFunctionStructure.getInitializedOnMainThread(this); }
@@ -601,10 +597,6 @@ public:
V(private, LazyPropertyOfGlobalObject<Structure>, m_commonJSModuleObjectStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSSocketAddressDTOStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSReactElementStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSMarkdownListItemMetaStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSMarkdownListMetaStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSMarkdownCellMetaStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_JSMarkdownLinkMetaStructure) \
V(private, LazyPropertyOfGlobalObject<Structure>, m_memoryFootprintStructure) \
V(private, LazyPropertyOfGlobalObject<JSObject>, m_requireFunctionUnbound) \
V(private, LazyPropertyOfGlobalObject<JSObject>, m_requireResolveFunctionUnbound) \

View File

@@ -146,130 +146,6 @@ describe("Bun.markdown.render", () => {
expect(result).toBe('<ol start="3"><li>first</li><li>second</li></ol>');
});
test("listItem receives {index, depth, ordered, start, checked}", () => {
const metas: any[] = [];
Markdown.render("3. first\n4. second\n5. third\n", {
listItem: (c: string, m: any) => {
metas.push(m);
return c;
},
list: (c: string) => c,
});
// Shape is fixed (5 properties) so JSC inline caches stay monomorphic;
// `start` is undefined for unordered, `checked` is undefined for non-task items.
expect(metas).toEqual([
{ index: 0, depth: 0, ordered: true, start: 3, checked: undefined },
{ index: 1, depth: 0, ordered: true, start: 3, checked: undefined },
{ index: 2, depth: 0, ordered: true, start: 3, checked: undefined },
]);
// All items share the same hidden class.
expect(Object.keys(metas[0])).toEqual(["index", "depth", "ordered", "start", "checked"]);
});
test("listItem meta for unordered list (start is undefined)", () => {
const metas: any[] = [];
Markdown.render("- a\n- b\n", {
listItem: (c: string, m: any) => {
metas.push(m);
return c;
},
list: (c: string) => c,
});
expect(metas).toEqual([
{ index: 0, depth: 0, ordered: false, start: undefined, checked: undefined },
{ index: 1, depth: 0, ordered: false, start: undefined, checked: undefined },
]);
});
test("listItem depth tracks nesting", () => {
const metas: any[] = [];
Markdown.render("1. outer\n 1. inner-a\n 2. inner-b\n2. outer2\n", {
listItem: (_: string, m: any) => {
metas.push(m);
return "";
},
list: () => "",
});
// Callbacks fire bottom-up: inner items first, then outer.
expect(metas).toEqual([
{ index: 0, depth: 1, ordered: true, start: 1, checked: undefined },
{ index: 1, depth: 1, ordered: true, start: 1, checked: undefined },
{ index: 0, depth: 0, ordered: true, start: 1, checked: undefined },
{ index: 1, depth: 0, ordered: true, start: 1, checked: undefined },
]);
});
test("list meta includes depth", () => {
const metas: any[] = [];
Markdown.render("- outer\n - inner\n", {
list: (c: string, m: any) => {
metas.push(m);
return c;
},
listItem: (c: string) => c,
});
// Inner list fires first (bottom-up). Fixed shape: start is always present.
expect(metas).toEqual([
{ ordered: false, start: undefined, depth: 1 },
{ ordered: false, start: undefined, depth: 0 },
]);
});
test("listItem meta includes checked alongside index/depth/ordered", () => {
const metas: any[] = [];
Markdown.render(
"- [x] done\n- [ ] todo\n- plain\n",
{
listItem: (c: string, m: any) => {
metas.push(m);
return c;
},
list: (c: string) => c,
},
{ tasklists: true },
);
expect(metas).toEqual([
{ index: 0, depth: 0, ordered: false, start: undefined, checked: true },
{ index: 1, depth: 0, ordered: false, start: undefined, checked: false },
{ index: 2, depth: 0, ordered: false, start: undefined, checked: undefined },
]);
});
test("listItem index resets across sibling lists", () => {
const metas: any[] = [];
Markdown.render("1. a\n2. b\n\npara\n\n1. c\n2. d\n", {
listItem: (c: string, m: any) => {
metas.push({ text: c, index: m.index });
return c;
},
list: (c: string) => c,
paragraph: (c: string) => c,
});
expect(metas).toEqual([
{ text: "a", index: 0 },
{ text: "b", index: 1 },
{ text: "c", index: 0 },
{ text: "d", index: 1 },
]);
});
test("listItem enables direct marker rendering (no post-processing)", () => {
// The motivating use case: ANSI terminal renderer with depth-aware numbering.
const toAlpha = (n: number) => String.fromCharCode(96 + n);
const result = Markdown.render("1. first\n 1. sub-a\n 2. sub-b\n2. second\n", {
listItem: (c: string, m: any) => {
const n = (m.start ?? 1) + m.index;
const marker = !m.ordered ? "-" : m.depth === 0 ? `${n}.` : `${toAlpha(n)}.`;
const indent = " ".repeat(m.depth);
return indent + marker + " " + c.trimEnd() + "\n";
},
// Nested lists are concatenated directly after the parent item's text;
// prefix a newline so the outer listItem's trimEnd() works correctly.
list: (c: string) => "\n" + c,
});
expect(result).toBe("\n1. first\n a. sub-a\n b. sub-b\n2. second\n");
});
test("strikethrough callback", () => {
const result = Markdown.render("~~deleted~~\n", {
strikethrough: (children: string) => `<del>${children}</del>`,

View File

@@ -0,0 +1,103 @@
import { describe, expect, test } from "bun:test";
describe("issue #27687 - virtual HTML entrypoint with absolute script src", () => {
test("resolves virtual script referenced via absolute path from virtual HTML", async () => {
const result = await Bun.build({
entrypoints: ["/virtual/index.html"],
target: "browser",
format: "esm",
minify: false,
files: {
"/virtual/index.html": `
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"></head>
<body>
<div id="root"></div>
<script type="module" src="/virtual/_hydrate.tsx"></script>
</body>
</html>`,
"/virtual/_hydrate.tsx": `console.log("Hydration entry loaded");`,
},
});
if (!result.success) {
console.error(result.logs);
}
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(2);
const htmlOutput = result.outputs.find(o => o.type?.startsWith("text/html"));
expect(htmlOutput).toBeDefined();
const jsOutput = result.outputs.find(o => o.type?.startsWith("text/javascript"));
expect(jsOutput).toBeDefined();
const jsContent = await jsOutput!.text();
expect(jsContent).toContain("Hydration entry loaded");
});
test("resolves virtual script with absolute path from different virtual directory", async () => {
const result = await Bun.build({
entrypoints: ["/app/index.html"],
target: "browser",
format: "esm",
minify: false,
files: {
"/app/index.html": `
<!DOCTYPE html>
<html>
<body>
<script type="module" src="/shared/utils.js"></script>
</body>
</html>`,
"/shared/utils.js": `export const msg = "cross-directory import works";
console.log(msg);`,
},
});
if (!result.success) {
console.error(result.logs);
}
expect(result.success).toBe(true);
const jsOutput = result.outputs.find(o => o.type?.startsWith("text/javascript"));
expect(jsOutput).toBeDefined();
const jsContent = await jsOutput!.text();
expect(jsContent).toContain("cross-directory import works");
});
test("resolves virtual script with root-level absolute path from virtual HTML", async () => {
const result = await Bun.build({
entrypoints: ["/index.html"],
target: "browser",
format: "esm",
minify: false,
files: {
"/index.html": `
<!DOCTYPE html>
<html>
<body>
<script type="module" src="/app.js"></script>
</body>
</html>`,
"/app.js": `console.log("root level script");`,
},
});
if (!result.success) {
console.error(result.logs);
}
expect(result.success).toBe(true);
const jsOutput = result.outputs.find(o => o.type?.startsWith("text/javascript"));
expect(jsOutput).toBeDefined();
const jsContent = await jsOutput!.text();
expect(jsContent).toContain("root level script");
});
});