feat(build): add --metafile-md CLI option for LLM-friendly bundle analysis - #26441
Conversation
…lysis Add a new `--metafile-md` option to `bun build` that generates a markdown visualization of the module graph, designed to help Claude and other LLMs analyze bundle composition. Features: - Quick Summary with module counts, sizes, and output/input ratio - Largest Input Files sorted by size to identify bloat - Entry Point Analysis showing bundle size and bundled modules - Dependency Chains showing which files import each module - Full Module Graph with complete import/export information - Raw Data section with grep-friendly markers for searching: - [MODULE:], [SIZE:], [IMPORT:], [IMPORTED_BY:] - [ENTRY:], [EXTERNAL:], [NODE_MODULES:] Usage: bun build entry.js --metafile-md # writes meta.md bun build entry.js --metafile-md=analysis.md # custom filename bun build entry.js --metafile --metafile-md # both JSON and markdown Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
WalkthroughAdds Markdown metafile generation and plumbing: public MetafileBuilder.generateMarkdown, new CLI flag --metafile-md, build flow to emit Markdown alongside JSON, linker/bundle/JS bindings updated to carry both representations, and tests/fixtures extended. Changes
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/bundler/linker_context/MetafileBuilder.zig`:
- Around line 398-404: The listed helper structs (InputFileInfo, ModuleSize,
ImportedByInfo, PathOnly) expose public fields but should use Zig private
fields; update each struct to prefix all field names with `#` and then update
every access site to use the corresponding private field names (e.g., replace
usages of .path, .bytes, .import_count, .is_node_modules, .format, etc. with
.#path, .#bytes, .#import_count, .#is_node_modules, .#format) and similarly for
fields in ModuleSize, ImportedByInfo, and PathOnly; ensure constructors,
comparators, assignments, and any pattern-matching or field accesses in
functions that reference these structs are updated to the new `#`-prefixed names
so the code compiles and retains the same behavior.
- Around line 486-505: The reverse-dependency matching hardcodes '/' as the path
separator and stripParentRefs only strips "./" and "../", causing failures on
Windows; update the boundary check that currently compares target[target.len -
input_key.len - 1] == '/' to use bun.path.isSepAny(byte) (or an equivalent
platform-aware is-separator helper) so it accepts both '/' and '\\', and modify
stripParentRefs to normalize and strip both forward- and backslash parent refs
(e.g., "../", "..\\", "./", ".\\") before calling std.mem.endsWith; ensure
comparisons involving std.mem.endsWith and std.fs.path.basename still operate on
normalized separators so matched_key logic (and use of target, input_key,
target_without_dots, target_base, key_base) works correctly across platforms.
In `@test/bundler/metafile.test.ts`:
- Around line 857-860: The test uses a repetitive string via
"${"x".repeat(500)}" in the "large.js" module; replace the .repeat usage with
Buffer.alloc(500, "x").toString() to follow the repo guideline for repetitive
test strings, i.e., update the "large.js" content string construction (the
exported symbol large in the test data) to use Buffer.alloc(count,
fill).toString() instead of .repeat().
| const InputFileInfo = struct { | ||
| path: []const u8, | ||
| bytes: u64, | ||
| import_count: u32, | ||
| is_node_modules: bool, | ||
| format: []const u8, | ||
| }; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Apply #-prefixed private fields to helper structs.
InputFileInfo, ModuleSize, ImportedByInfo, and PathOnly are internal helpers but declare public fields. Please prefix these fields with # and update access sites accordingly. As per coding guidelines, private Zig struct fields should be #-prefixed.
Also applies to: 699-700, 741-742, 791-792
🤖 Prompt for AI Agents
In `@src/bundler/linker_context/MetafileBuilder.zig` around lines 398 - 404, The
listed helper structs (InputFileInfo, ModuleSize, ImportedByInfo, PathOnly)
expose public fields but should use Zig private fields; update each struct to
prefix all field names with `#` and then update every access site to use the
corresponding private field names (e.g., replace usages of .path, .bytes,
.import_count, .is_node_modules, .format, etc. with .#path, .#bytes,
.#import_count, .#is_node_modules, .#format) and similarly for fields in
ModuleSize, ImportedByInfo, and PathOnly; ensure constructors, comparators,
assignments, and any pattern-matching or field accesses in functions that
reference these structs are updated to the new `#`-prefixed names so the code
compiles and retains the same behavior.
| if (std.mem.endsWith(u8, target, input_key)) { | ||
| // Make sure it's a path boundary (preceded by / or start) | ||
| if (target.len == input_key.len or | ||
| (target.len > input_key.len and target[target.len - input_key.len - 1] == '/')) | ||
| { | ||
| matched_key = input_key; | ||
| break; | ||
| } | ||
| } | ||
| // Also check if input_key ends with target (for relative paths) | ||
| // e.g., target="../utils/logger.js" might match "src/utils/logger.js" | ||
| if (std.mem.indexOf(u8, target, "..") != null) { | ||
| // This is a relative path, try matching just the filename parts | ||
| const target_base = std.fs.path.basename(target); | ||
| const key_base = std.fs.path.basename(input_key); | ||
| if (std.mem.eql(u8, target_base, key_base)) { | ||
| // Check if paths share common suffix | ||
| const target_without_dots = stripParentRefs(target); | ||
| if (std.mem.endsWith(u8, input_key, target_without_dots)) { | ||
| matched_key = input_key; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check the file size and read the relevant sections
wc -l src/bundler/linker_context/MetafileBuilder.zigRepository: oven-sh/bun
Length of output: 106
🏁 Script executed:
# Read the code around lines 486-505
sed -n '480,510p' src/bundler/linker_context/MetafileBuilder.zigRepository: oven-sh/bun
Length of output: 2440
🏁 Script executed:
# Read the code around lines 1035-1043
sed -n '1030,1050p' src/bundler/linker_context/MetafileBuilder.zigRepository: oven-sh/bun
Length of output: 631
🏁 Script executed:
# Search for stripParentRefs function
rg -n "stripParentRefs" src/bundler/linker_context/MetafileBuilder.zig -A 10Repository: oven-sh/bun
Length of output: 1107
🏁 Script executed:
# Search for the helper struct definitions mentioned in the review
rg -n "InputFileInfo|ModuleSize|ImportedByInfo|PathOnly" src/bundler/linker_context/MetafileBuilder.zig -B 2 -A 5Repository: oven-sh/bun
Length of output: 3623
🏁 Script executed:
# Check if bun.path utilities are used elsewhere in this file
rg -n "bun\.path|std\.fs\.path" src/bundler/linker_context/MetafileBuilder.zigRepository: oven-sh/bun
Length of output: 250
🏁 Script executed:
# Search for bun.path usage in the codebase to see patterns
rg -n "bun\.path\." --type zig | head -20Repository: oven-sh/bun
Length of output: 2249
🏁 Script executed:
# Check if bun.path module exists and what utilities it offers
rg -n "fn.*sep|pub const sep" src/bun/ --type zig | head -20Repository: oven-sh/bun
Length of output: 103
🏁 Script executed:
# Check how other files in src/bundler handle path separators
rg -n "std\.fs\.path\.sep|bun\.path\.sep" src/bundler/ --type zigRepository: oven-sh/bun
Length of output: 37
🏁 Script executed:
# Look for alternative path handling in bun module
find src -name "*.zig" -type f | xargs grep -l "fn basename\|fn dirname" | head -5Repository: oven-sh/bun
Length of output: 178
🏁 Script executed:
# Look for bun.path module and examine its separator handling utilities
rg -n "isSepAny|pub.*sep" src/ --type zig -A 2 | grep -A 2 "isSepAny"Repository: oven-sh/bun
Length of output: 9573
🏁 Script executed:
# Check the actual implementation in bun.zig or path module
find src -name "*.zig" -type f -exec grep -l "pub fn isSepAny\|pub const sep" {} \; | head -5Repository: oven-sh/bun
Length of output: 109
🏁 Script executed:
# Let's look at how path separators should be handled
rg -n "fn.*basename|pub.*basename" src/ --type zig -B 1 -A 3 | head -40Repository: oven-sh/bun
Length of output: 2269
Normalize path separators for reverse-dependency matching (Windows correctness).
The boundary check at line 491 hardcodes '/', which fails on Windows paths using \ separators. Additionally, stripParentRefs only handles "../" and "./", missing Windows variants. Use bun.path.isSepAny() for platform-aware separator checking.
🛠️ Suggested fix
- if (std.mem.endsWith(u8, target, input_key)) {
- // Make sure it's a path boundary (preceded by / or start)
- if (target.len == input_key.len or
- (target.len > input_key.len and target[target.len - input_key.len - 1] == '/'))
- {
+ if (std.mem.endsWith(u8, target, input_key)) {
+ // Make sure it's a path boundary (preceded by path separator or start)
+ const boundary_ok = target.len == input_key.len or
+ (target.len > input_key.len and
+ bun.path.isSepAny(target[target.len - input_key.len - 1]));
+ if (boundary_ok) {
matched_key = input_key;
break;
}
} fn stripParentRefs(path: []const u8) []const u8 {
var result = path;
- while (result.len >= 3 and std.mem.startsWith(u8, result, "../")) {
+ while (result.len >= 3 and (std.mem.startsWith(u8, result, "../") or std.mem.startsWith(u8, result, "..\\"))) {
result = result[3..];
}
// Also handle ./ prefix
- while (result.len >= 2 and std.mem.startsWith(u8, result, "./")) {
+ while (result.len >= 2 and (std.mem.startsWith(u8, result, "./") or std.mem.startsWith(u8, result, ".\\"))) {
result = result[2..];
}
return result;
}🤖 Prompt for AI Agents
In `@src/bundler/linker_context/MetafileBuilder.zig` around lines 486 - 505, The
reverse-dependency matching hardcodes '/' as the path separator and
stripParentRefs only strips "./" and "../", causing failures on Windows; update
the boundary check that currently compares target[target.len - input_key.len -
1] == '/' to use bun.path.isSepAny(byte) (or an equivalent platform-aware
is-separator helper) so it accepts both '/' and '\\', and modify stripParentRefs
to normalize and strip both forward- and backslash parent refs (e.g., "../",
"..\\", "./", ".\\") before calling std.mem.endsWith; ensure comparisons
involving std.mem.endsWith and std.fs.path.basename still operate on normalized
separators so matched_key logic (and use of target, input_key,
target_without_dots, target_base, key_base) works correctly across platforms.
| "entry.js": `import "./small.js"; import "./large.js";`, | ||
| "small.js": `export const s = 1;`, | ||
| "large.js": `export const large = "${"x".repeat(500)}";`, | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Use Buffer.alloc(...).toString() instead of .repeat() for large test strings.
This matches the repo test guideline for repetitive strings.
♻️ Suggested change
- "large.js": `export const large = "${"x".repeat(500)}";`,
+ "large.js": `export const large = "${Buffer.alloc(500, "x").toString()}";`,As per coding guidelines, prefer Buffer.alloc(count, fill).toString() for repetitive strings in tests.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "entry.js": `import "./small.js"; import "./large.js";`, | |
| "small.js": `export const s = 1;`, | |
| "large.js": `export const large = "${"x".repeat(500)}";`, | |
| }); | |
| "entry.js": `import "./small.js"; import "./large.js";`, | |
| "small.js": `export const s = 1;`, | |
| "large.js": `export const large = "${Buffer.alloc(500, "x").toString()}";`, | |
| }); |
🤖 Prompt for AI Agents
In `@test/bundler/metafile.test.ts` around lines 857 - 860, The test uses a
repetitive string via "${"x".repeat(500)}" in the "large.js" module; replace the
.repeat usage with Buffer.alloc(500, "x").toString() to follow the repo
guideline for repetitive test strings, i.e., update the "large.js" content
string construction (the exported symbol large in the test data) to use
Buffer.alloc(count, fill).toString() instead of .repeat().
std.StringHashMap is banned in the codebase - bun.StringHashMap has a faster `eql` implementation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed --metafile-md to display bytes contributed to the output bundle (bytesInOutput) instead of raw input file sizes. This is more useful for diagnosing bundle bloat since it shows the actual impact on bundle size. Changes: - Replace all raw input file size references with output contribution - Rename "Largest Input Files" to "Largest Modules by Output Contribution" - Remove "Total input size" and "Output/Input ratio" from summary - Use [OUTPUT_BYTES:] instead of [SIZE:] in grep-friendly raw data - Update imports display to show "contributes X" instead of raw size - Update tests to match new output format Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/bundler/linker_context/MetafileBuilder.zig`:
- Around line 416-421: The metadata-building loops currently use fallible
operations (e.g., bytes_in_output.getOrPut, appending to imported_by and
input_files) with `try` which aborts on OOM; change these to non-fatal
operations by using `catch continue` (or otherwise checking the error and
skipping that metadata entry) so markdown-only metadata paths degrade gracefully
instead of failing the build — update the occurrences around the
bytes_in_output.getOrPut block and the append sites referenced (including the
similar patterns at the other occurrences noted) to handle allocation failures
by skipping that iteration and proceeding.
♻️ Duplicate comments (3)
src/bundler/linker_context/MetafileBuilder.zig (2)
432-438: Use#-prefixed private fields for helper structs.
InputFileInfo,ModuleSize,ImportedByInfo, andPathOnlyare internal helpers but expose public fields. Please prefix fields with#and update all access sites accordingly. As per coding guidelines, prefer#-prefixed private fields.Also applies to: 731-733, 773-774, 823-824
517-535: Normalize path separators for reverse-dependency matching on Windows.The boundary check hardcodes
/, andstripParentRefsonly strips../and./, so imports using\won’t match on Windows. Considerbun.path.isSepAnyand handling..\\/.\\prefixes to keep reverse-dependency results accurate across platforms.🛠️ Suggested fix
- if (std.mem.endsWith(u8, target, input_key)) { - // Make sure it's a path boundary (preceded by / or start) - if (target.len == input_key.len or - (target.len > input_key.len and target[target.len - input_key.len - 1] == '/')) - { + if (std.mem.endsWith(u8, target, input_key)) { + // Make sure it's a path boundary (preceded by path separator or start) + const boundary_ok = target.len == input_key.len or + (target.len > input_key.len and + bun.path.isSepAny(target[target.len - input_key.len - 1])); + if (boundary_ok) { matched_key = input_key; break; } }- while (result.len >= 3 and std.mem.startsWith(u8, result, "../")) { + while (result.len >= 3 and (std.mem.startsWith(u8, result, "../") or std.mem.startsWith(u8, result, "..\\"))) { result = result[3..]; } // Also handle ./ prefix - while (result.len >= 2 and std.mem.startsWith(u8, result, "./")) { + while (result.len >= 2 and (std.mem.startsWith(u8, result, "./") or std.mem.startsWith(u8, result, ".\\"))) { result = result[2..]; }Also applies to: 1064-1073
test/bundler/metafile.test.ts (1)
854-859: UseBuffer.alloc(...).toString()for the large test string.Replace
.repeat(500)withBuffer.alloc(500, "x").toString()to align with test guidelines. As per coding guidelines, preferBuffer.alloc(count, fill).toString()for repetitive strings.♻️ Suggested change
- "large.js": `export const large = "${"x".repeat(500)}";`, + "large.js": `export const large = "${Buffer.alloc(500, "x").toString()}";`,
| const gop = try bytes_in_output.getOrPut(module_path); | ||
| if (gop.found_existing) { | ||
| gop.value_ptr.* += bytes_val; | ||
| } else { | ||
| gop.value_ptr.* = bytes_val; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Gracefully handle OOM in markdown-only metadata paths.
Markdown generation is supplementary, but the current try allocations in loops (e.g., bytes_in_output.getOrPut, imported_by append, input_files.append) will abort the build on OOM. Consider catch continue so the report degrades rather than failing the build. Based on learnings, prefer non-fatal handling for optional metadata in bundler code.
♻️ Suggested refactor
- const gop = try bytes_in_output.getOrPut(module_path);
+ const gop = bytes_in_output.getOrPut(module_path) catch continue;
...
- const gop = try imported_by.getOrPut(key);
+ const gop = imported_by.getOrPut(key) catch continue;
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
- try gop.value_ptr.append(allocator, path);
+ gop.value_ptr.append(allocator, path) catch continue;
...
- try input_files.append(allocator, info);
+ input_files.append(allocator, info) catch continue;Also applies to: 545-549, 558-559
🤖 Prompt for AI Agents
In `@src/bundler/linker_context/MetafileBuilder.zig` around lines 416 - 421, The
metadata-building loops currently use fallible operations (e.g.,
bytes_in_output.getOrPut, appending to imported_by and input_files) with `try`
which aborts on OOM; change these to non-fatal operations by using `catch
continue` (or otherwise checking the error and skipping that metadata entry) so
markdown-only metadata paths degrade gracefully instead of failing the build —
update the occurrences around the bytes_in_output.getOrPut block and the append
sites referenced (including the similar patterns at the other occurrences noted)
to handle allocation failures by skipping that iteration and proceeding.
Extends the Bun.build() JavaScript API to support metafile options:
- `metafile: true` - enables metafile generation (existing behavior)
- `metafile: "path.json"` - writes JSON to specified path
- `metafile: { json?: string, markdown?: string }` - writes to specified paths
The result object now has:
- `result.metafile.json` - lazily parsed metafile object
- `result.metafile.markdown` - markdown string (when requested)
Changes:
- JSBundler.zig: Parse metafile option variants (bool/string/object)
- BundlerMetafile.cpp: Use builtinNames for private properties
- bundle_v2.zig: Generate markdown and write files to disk
- LinkerContext.zig: Add metafile path options
- BunBuiltinNames.h: Add metafileJson private name
- metafile.test.ts: Add tests for new API, update existing tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/bundler/bundle_v2.zig`:
- Around line 1613-1640: Extract the duplicated metafile write logic into a
shared helper (e.g., fn writeMetafileOutputs(linker_options: anytype, metafile:
?[]const u8, metafile_markdown: ?[]const u8) void) and replace the repeated
blocks in the current function and runFromJSInNewThread with a call to that
helper; the helper should perform the JSON and markdown file writes using
std.fs.cwd().writeFile, emit bun.Output.warn on errors, and format errors with
`@errorName`(err) exactly like the existing blocks so behavior remains identical.
In `@src/js/builtins/BunBuiltinNames.h`:
- Around line 166-170: The builtin identifier list is marked as sorted but the
entry ordering is incorrect: move the macro entry for metafileJson to appear
before method in the macro list so the sequence reads
...macro(makeGetterTypeError) \ macro(maxAge) \ macro(metafileJson) \
macro(method) \ macro(mockedFunction) \; update the block containing these
macro(...) entries (look for makeGetterTypeError, maxAge, metafileJson, method,
mockedFunction) to reorder metafileJson lexicographically before method.
In `@test/bundler/metafile.test.ts`:
- Around line 731-734: The test currently gathers [stdout, stderr, exitCode]
simultaneously and asserts exitCode first; reorder the assertions so you check
stdout and stderr before asserting exitCode to surface output on failure. Keep
the existing Promise.all that awaits proc.stdout.text(), proc.stderr.text(), and
proc.exited, but perform expectations on the captured stdout and stderr
variables prior to asserting the exitCode variable (referencing stdout, stderr,
exitCode and proc.exited).
♻️ Duplicate comments (2)
test/bundler/metafile.test.ts (1)
1038-1043: Use Buffer.alloc(...).toString() for large test strings.🔧 Suggested change
- "large.js": `export const large = "${"x".repeat(500)}";`, + "large.js": `export const large = "${Buffer.alloc(500, "x").toString()}";`,As per coding guidelines, ...
src/bundler/bundle_v2.zig (1)
2800-2832: Duplicate code - see earlier refactoring suggestion.This block is nearly identical to lines 1613-1640 in
generateFromCLI. The suggested helper function would eliminate this duplication.
| macro(makeGetterTypeError) \ | ||
| macro(maxAge) \ | ||
| macro(method) \ | ||
| macro(metafileJson) \ | ||
| macro(mockedFunction) \ |
There was a problem hiding this comment.
Keep builtin identifier list sorted (metafileJson placement).
The list is marked as sorted, but metafileJson should come before method lexicographically.
🔧 Proposed reorder
- macro(method) \
- macro(metafileJson) \
+ macro(metafileJson) \
+ macro(method) \📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| macro(makeGetterTypeError) \ | |
| macro(maxAge) \ | |
| macro(method) \ | |
| macro(metafileJson) \ | |
| macro(mockedFunction) \ | |
| macro(makeGetterTypeError) \ | |
| macro(maxAge) \ | |
| macro(metafileJson) \ | |
| macro(method) \ | |
| macro(mockedFunction) \ |
🤖 Prompt for AI Agents
In `@src/js/builtins/BunBuiltinNames.h` around lines 166 - 170, The builtin
identifier list is marked as sorted but the entry ordering is incorrect: move
the macro entry for metafileJson to appear before method in the macro list so
the sequence reads ...macro(makeGetterTypeError) \ macro(maxAge) \
macro(metafileJson) \ macro(method) \ macro(mockedFunction) \; update the block
containing these macro(...) entries (look for makeGetterTypeError, maxAge,
metafileJson, method, mockedFunction) to reorder metafileJson lexicographically
before method.
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
|
|
||
| expect(exitCode).toBe(0); | ||
|
|
There was a problem hiding this comment.
Assert stdout/stderr before exitCode for clearer failures.
Add stdout/stderr expectations before exitCode so failures show output context.
🔧 Suggested adjustment (apply to similar blocks)
- const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
-
- expect(exitCode).toBe(0);
+ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+ expect(stdout).toBe("");
+ expect(stderr).toBe("");
+ expect(exitCode).toBe(0);As per coding guidelines, ...
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | |
| expect(exitCode).toBe(0); | |
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | |
| expect(stdout).toBe(""); | |
| expect(stderr).toBe(""); | |
| expect(exitCode).toBe(0); |
🤖 Prompt for AI Agents
In `@test/bundler/metafile.test.ts` around lines 731 - 734, The test currently
gathers [stdout, stderr, exitCode] simultaneously and asserts exitCode first;
reorder the assertions so you check stdout and stderr before asserting exitCode
to surface output on failure. Keep the existing Promise.all that awaits
proc.stdout.text(), proc.stderr.text(), and proc.exited, but perform
expectations on the captured stdout and stderr variables prior to asserting the
exitCode variable (referencing stdout, stderr, exitCode and proc.exited).
- Refactor metafile file writing to use the standard OutputFile system - Write metafile files using NodeFS.writeFileWithPathBuffer for consistency - Create OutputFile entries with .saved value type after writing - Fix memory allocation for input_path (must be heap-allocated for deinit) - Replace std.fs.cwd() with bun.FD.cwd() to comply with banned words - Maintain backward compatibility: result.metafile returns JSON directly The metafile paths are written relative to CWD (not outdir), matching esbuild behavior. Files appear in result.outputs array with proper output_kind (.metafile-json or .metafile-markdown). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/bundler/bundle_v2.zig`:
- Around line 2782-2871: The metafile write blocks currently skip disk writes
when root_path is empty but still append OutputFile entries and use relative
paths; fix by always resolving an absolute write path for both JSON and Markdown
(compute abs_path = if root_path.len > 0 then join(root_path, json_path/md_path)
else resolve against CWD), remove the conditional that entirely gates the write
on root_path.len, use that abs_path when creating parent directories
(std.fs.path.dirname(abs_path)) and when calling
jsc.Node.fs.NodeFS.writeFileWithPathBuffer, and keep the OutputFile.append
behavior unchanged but ensure its .output_path uses the same abs_path string;
update references in these blocks (this.linker.options.metafile_json_path,
this.linker.options.metafile_markdown_path,
jsc.Node.fs.NodeFS.writeFileWithPathBuffer, bun.FD.cwd().makePath, and
output_files.append) accordingly.
In `@src/cli/build_command.zig`:
- Around line 356-383: The build currently only warns when
MetafileBuilder.generateMarkdown fails (in the block checking
ctx.bundler_options.metafile_md) so the process can exit successfully even
though the user requested a --metafile-md; change the handler for
generateMarkdown errors to treat them like write failures: call Output.err with
the error and a descriptive message and then call exitOrWatch(1,
ctx.debug.hot_reload == .watch) (same behavior used for file open/write
failures) instead of Output.warn and continuing, ensuring the error path in the
generateMarkdown catch uses the same exit flow as the subsequent file
operations.
| if (this.linker.options.metafile_json_path.len > 0) { | ||
| if (metafile) |mf| { | ||
| const json_path = this.linker.options.metafile_json_path; | ||
| // Write to disk (metafile path is relative to CWD, not outdir) | ||
| const root_path = this.linker.resolver.opts.output_dir; | ||
| if (root_path.len > 0) { | ||
| // Create parent directories if needed | ||
| if (std.fs.path.dirname(json_path)) |parent| { | ||
| bun.FD.cwd().makePath(u8, parent) catch {}; | ||
| } | ||
| // Write directly to disk | ||
| var path_buf: bun.PathBuffer = undefined; | ||
| _ = jsc.Node.fs.NodeFS.writeFileWithPathBuffer(&path_buf, .{ | ||
| .data = .{ .buffer = .{ | ||
| .buffer = .{ | ||
| .ptr = @constCast(mf.ptr), | ||
| .len = @as(u32, @truncate(mf.len)), | ||
| .byte_len = @as(u32, @truncate(mf.len)), | ||
| }, | ||
| } }, | ||
| .encoding = .buffer, | ||
| .mode = 0o644, | ||
| .dirfd = bun.FD.cwd(), | ||
| .file = .{ .path = .{ | ||
| .string = bun.PathString.init(json_path), | ||
| } }, | ||
| }).unwrap() catch |err| { | ||
| bun.Output.warn("Failed to write metafile JSON to '{s}': {s}", .{ json_path, @errorName(err) }); | ||
| }; | ||
| } | ||
| // Add as OutputFile so it appears in result.outputs | ||
| try output_files.append(options.OutputFile.init(.{ | ||
| .loader = .json, | ||
| .input_loader = .json, | ||
| .input_path = bun.handleOom(bun.default_allocator.dupe(u8, "metafile.json")), | ||
| .output_path = bun.handleOom(bun.default_allocator.dupe(u8, json_path)), | ||
| .data = .{ .saved = mf.len }, | ||
| .output_kind = .@"metafile-json", | ||
| .is_executable = false, | ||
| .side = null, | ||
| .entry_point_index = null, | ||
| })); | ||
| } | ||
| } | ||
|
|
||
| // Add metafile markdown as OutputFile if path specified | ||
| if (this.linker.options.metafile_markdown_path.len > 0) { | ||
| if (metafile_markdown) |md| { | ||
| const md_path = this.linker.options.metafile_markdown_path; | ||
| // Write to disk (metafile path is relative to CWD, not outdir) | ||
| const root_path = this.linker.resolver.opts.output_dir; | ||
| if (root_path.len > 0) { | ||
| // Create parent directories if needed | ||
| if (std.fs.path.dirname(md_path)) |parent| { | ||
| bun.FD.cwd().makePath(u8, parent) catch {}; | ||
| } | ||
| // Write directly to disk | ||
| var path_buf: bun.PathBuffer = undefined; | ||
| _ = jsc.Node.fs.NodeFS.writeFileWithPathBuffer(&path_buf, .{ | ||
| .data = .{ .buffer = .{ | ||
| .buffer = .{ | ||
| .ptr = @constCast(md.ptr), | ||
| .len = @as(u32, @truncate(md.len)), | ||
| .byte_len = @as(u32, @truncate(md.len)), | ||
| }, | ||
| } }, | ||
| .encoding = .buffer, | ||
| .mode = 0o644, | ||
| .dirfd = bun.FD.cwd(), | ||
| .file = .{ .path = .{ | ||
| .string = bun.PathString.init(md_path), | ||
| } }, | ||
| }).unwrap() catch |err| { | ||
| bun.Output.warn("Failed to write metafile markdown to '{s}': {s}", .{ md_path, @errorName(err) }); | ||
| }; | ||
| } | ||
| // Add as OutputFile so it appears in result.outputs | ||
| try output_files.append(options.OutputFile.init(.{ | ||
| .loader = .file, | ||
| .input_loader = .file, | ||
| .input_path = bun.handleOom(bun.default_allocator.dupe(u8, "metafile.md")), | ||
| .output_path = bun.handleOom(bun.default_allocator.dupe(u8, md_path)), | ||
| .data = .{ .saved = md.len }, | ||
| .output_kind = .@"metafile-markdown", | ||
| .is_executable = false, | ||
| .side = null, | ||
| .entry_point_index = null, | ||
| })); | ||
| } | ||
| } |
There was a problem hiding this comment.
Metafile writes are skipped when output_dir is empty; also use absolute paths for file ops.
The root_path.len > 0 guard prevents writing metafile JSON/Markdown when no outdir is set (common for in-memory builds), yet OutputFile entries are still appended. Also, the write path is relative; src/**/*.zig file ops must use absolute paths.
🔧 Proposed fix (apply same pattern to JSON and Markdown blocks)
- const json_path = this.linker.options.metafile_json_path;
- // Write to disk (metafile path is relative to CWD, not outdir)
- const root_path = this.linker.resolver.opts.output_dir;
- if (root_path.len > 0) {
- // Create parent directories if needed
- if (std.fs.path.dirname(json_path)) |parent| {
- bun.FD.cwd().makePath(u8, parent) catch {};
- }
- // Write directly to disk
- var path_buf: bun.PathBuffer = undefined;
- _ = jsc.Node.fs.NodeFS.writeFileWithPathBuffer(&path_buf, .{
- .data = .{ .buffer = .{
- .buffer = .{
- .ptr = `@constCast`(mf.ptr),
- .len = `@as`(u32, `@truncate`(mf.len)),
- .byte_len = `@as`(u32, `@truncate`(mf.len)),
- },
- } },
- .encoding = .buffer,
- .mode = 0o644,
- .dirfd = bun.FD.cwd(),
- .file = .{ .path = .{
- .string = bun.PathString.init(json_path),
- } },
- }).unwrap() catch |err| {
- bun.Output.warn("Failed to write metafile JSON to '{s}': {s}", .{ json_path, `@errorName`(err) });
- };
- }
+ const json_path = this.linker.options.metafile_json_path;
+ // Write to disk (metafile path is relative to CWD, not outdir)
+ var path_buf: bun.PathBuffer = undefined;
+ const json_abs = if (std.fs.path.isAbsolute(json_path))
+ json_path
+ else
+ bun.path.joinAbsStringBuf(
+ bun.fs.FileSystem.instance.top_level_dir,
+ &path_buf,
+ &[_][]const u8{json_path},
+ .auto,
+ );
+ if (std.fs.path.dirname(json_abs)) |parent| {
+ bun.FD.cwd().makePath(u8, parent) catch {};
+ }
+ _ = jsc.Node.fs.NodeFS.writeFileWithPathBuffer(&path_buf, .{
+ .data = .{ .buffer = .{
+ .buffer = .{
+ .ptr = `@constCast`(mf.ptr),
+ .len = `@as`(u32, `@truncate`(mf.len)),
+ .byte_len = `@as`(u32, `@truncate`(mf.len)),
+ },
+ } },
+ .encoding = .buffer,
+ .mode = 0o644,
+ .dirfd = bun.FD.cwd(),
+ .file = .{ .path = .{
+ .string = bun.PathString.init(json_abs),
+ } },
+ }).unwrap() catch |err| {
+ bun.Output.warn("Failed to write metafile JSON to '{s}': {s}", .{ json_path, `@errorName`(err) });
+ };As per coding guidelines, use absolute paths in file operations.
🤖 Prompt for AI Agents
In `@src/bundler/bundle_v2.zig` around lines 2782 - 2871, The metafile write
blocks currently skip disk writes when root_path is empty but still append
OutputFile entries and use relative paths; fix by always resolving an absolute
write path for both JSON and Markdown (compute abs_path = if root_path.len > 0
then join(root_path, json_path/md_path) else resolve against CWD), remove the
conditional that entirely gates the write on root_path.len, use that abs_path
when creating parent directories (std.fs.path.dirname(abs_path)) and when
calling jsc.Node.fs.NodeFS.writeFileWithPathBuffer, and keep the
OutputFile.append behavior unchanged but ensure its .output_path uses the same
abs_path string; update references in these blocks
(this.linker.options.metafile_json_path,
this.linker.options.metafile_markdown_path,
jsc.Node.fs.NodeFS.writeFileWithPathBuffer, bun.FD.cwd().makePath, and
output_files.append) accordingly.
| // Write markdown metafile if requested | ||
| if (ctx.bundler_options.metafile_md.len > 0) { | ||
| const metafile_md = MetafileBuilder.generateMarkdown(allocator, metafile_json) catch |err| blk: { | ||
| Output.warn("Failed to generate markdown metafile: {s}", .{@errorName(err)}); | ||
| break :blk null; | ||
| }; | ||
| if (metafile_md) |md_content| { | ||
| defer allocator.free(md_content); | ||
| const file = switch (bun.sys.File.makeOpen(ctx.bundler_options.metafile_md, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o664)) { | ||
| .result => |f| f, | ||
| .err => |err| { | ||
| Output.err(err, "could not open metafile-md {f}", .{bun.fmt.quote(ctx.bundler_options.metafile_md)}); | ||
| exitOrWatch(1, ctx.debug.hot_reload == .watch); | ||
| unreachable; | ||
| }, | ||
| }; | ||
| defer file.close(); | ||
|
|
||
| switch (file.writeAll(md_content)) { | ||
| .result => {}, | ||
| .err => |err| { | ||
| Output.err(err, "could not write metafile-md {f}", .{bun.fmt.quote(ctx.bundler_options.metafile_md)}); | ||
| exitOrWatch(1, ctx.debug.hot_reload == .watch); | ||
| unreachable; | ||
| }, | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Fail the build if markdown generation fails when explicitly requested.
Right now a generation failure only warns and still exits successfully, which can silently skip the requested --metafile-md output. Consider treating this like other metafile write failures.
🐛 Proposed fix
- const metafile_md = MetafileBuilder.generateMarkdown(allocator, metafile_json) catch |err| blk: {
- Output.warn("Failed to generate markdown metafile: {s}", .{`@errorName`(err)});
- break :blk null;
- };
- if (metafile_md) |md_content| {
- defer allocator.free(md_content);
+ const md_content = MetafileBuilder.generateMarkdown(allocator, metafile_json) catch |err| {
+ Output.err(err, "could not generate metafile-md", .{});
+ exitOrWatch(1, ctx.debug.hot_reload == .watch);
+ unreachable;
+ };
+ defer allocator.free(md_content);
const file = switch (bun.sys.File.makeOpen(ctx.bundler_options.metafile_md, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o664)) {
.result => |f| f,
.err => |err| {
Output.err(err, "could not open metafile-md {f}", .{bun.fmt.quote(ctx.bundler_options.metafile_md)});
exitOrWatch(1, ctx.debug.hot_reload == .watch);
unreachable;
},
};
defer file.close();
- switch (file.writeAll(md_content)) {
+ switch (file.writeAll(md_content)) {
.result => {},
.err => |err| {
Output.err(err, "could not write metafile-md {f}", .{bun.fmt.quote(ctx.bundler_options.metafile_md)});
exitOrWatch(1, ctx.debug.hot_reload == .watch);
unreachable;
},
}
- }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Write markdown metafile if requested | |
| if (ctx.bundler_options.metafile_md.len > 0) { | |
| const metafile_md = MetafileBuilder.generateMarkdown(allocator, metafile_json) catch |err| blk: { | |
| Output.warn("Failed to generate markdown metafile: {s}", .{@errorName(err)}); | |
| break :blk null; | |
| }; | |
| if (metafile_md) |md_content| { | |
| defer allocator.free(md_content); | |
| const file = switch (bun.sys.File.makeOpen(ctx.bundler_options.metafile_md, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o664)) { | |
| .result => |f| f, | |
| .err => |err| { | |
| Output.err(err, "could not open metafile-md {f}", .{bun.fmt.quote(ctx.bundler_options.metafile_md)}); | |
| exitOrWatch(1, ctx.debug.hot_reload == .watch); | |
| unreachable; | |
| }, | |
| }; | |
| defer file.close(); | |
| switch (file.writeAll(md_content)) { | |
| .result => {}, | |
| .err => |err| { | |
| Output.err(err, "could not write metafile-md {f}", .{bun.fmt.quote(ctx.bundler_options.metafile_md)}); | |
| exitOrWatch(1, ctx.debug.hot_reload == .watch); | |
| unreachable; | |
| }, | |
| } | |
| } | |
| } | |
| // Write markdown metafile if requested | |
| if (ctx.bundler_options.metafile_md.len > 0) { | |
| const md_content = MetafileBuilder.generateMarkdown(allocator, metafile_json) catch |err| { | |
| Output.err(err, "could not generate metafile-md", .{}); | |
| exitOrWatch(1, ctx.debug.hot_reload == .watch); | |
| unreachable; | |
| }; | |
| defer allocator.free(md_content); | |
| const file = switch (bun.sys.File.makeOpen(ctx.bundler_options.metafile_md, bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC, 0o664)) { | |
| .result => |f| f, | |
| .err => |err| { | |
| Output.err(err, "could not open metafile-md {f}", .{bun.fmt.quote(ctx.bundler_options.metafile_md)}); | |
| exitOrWatch(1, ctx.debug.hot_reload == .watch); | |
| unreachable; | |
| }, | |
| }; | |
| defer file.close(); | |
| switch (file.writeAll(md_content)) { | |
| .result => {}, | |
| .err => |err| { | |
| Output.err(err, "could not write metafile-md {f}", .{bun.fmt.quote(ctx.bundler_options.metafile_md)}); | |
| exitOrWatch(1, ctx.debug.hot_reload == .watch); | |
| unreachable; | |
| }, | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@src/cli/build_command.zig` around lines 356 - 383, The build currently only
warns when MetafileBuilder.generateMarkdown fails (in the block checking
ctx.bundler_options.metafile_md) so the process can exit successfully even
though the user requested a --metafile-md; change the handler for
generateMarkdown errors to treat them like write failures: call Output.err with
the error and a descriptive message and then call exitOrWatch(1,
ctx.debug.hot_reload == .watch) (same behavior used for file open/write
failures) instead of Output.warn and continuing, ensuring the error path in the
generateMarkdown catch uses the same exit flow as the subsequent file
operations.
|
Can we also add to the programmatic Bun.build() API (maybe |
The path matching logic in generateMarkdown was only checking for '/' as a path separator when matching import paths to input keys. On Windows, paths use '\' as the separator, causing the reverse dependency mapping to fail. This fix checks for both '/' and '\' as path separators when determining if an import path ends with an input key at a path boundary. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/bundler/linker_context/MetafileBuilder.zig`:
- Around line 530-531: Replace OS-specific std.fs.path.basename usage with the
path utility that handles mixed separators: use bun.path.basename for computing
target_base and key_base so import specifiers using '/' on Windows are handled
correctly; update the two calls that compute target_base and key_base (currently
calling std.fs.path.basename) to call bun.path.basename from the resolve_path
utility instead.
| const target_base = std.fs.path.basename(target); | ||
| const key_base = std.fs.path.basename(input_key); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find bun.path module definition and available functions
rg -n "pub fn|fn.*basename|isSepAny" --type zig src/ | grep -E "(bun\.path|basename)" | head -30Repository: oven-sh/bun
Length of output: 3459
🏁 Script executed:
#!/bin/bash
# Search for where bun.path is defined
fd -t f "path.zig" src/ | head -5Repository: oven-sh/bun
Length of output: 171
🏁 Script executed:
#!/bin/bash
# Check MetafileBuilder.zig context around lines 530-531
sed -n '520,540p' src/bundler/linker_context/MetafileBuilder.zigRepository: oven-sh/bun
Length of output: 1757
🏁 Script executed:
#!/bin/bash
# Look for basename usage patterns in the Bun codebase
rg -n "basename" --type zig src/bundler/linker_context/MetafileBuilder.zig -B 3 -A 3Repository: oven-sh/bun
Length of output: 1364
🏁 Script executed:
#!/bin/bash
# Check if bun.path.basename exists
rg -n "pub fn basename" --type zig src/ | grep -i pathRepository: oven-sh/bun
Length of output: 1542
🏁 Script executed:
#!/bin/bash
# Check the basename function in src/string/immutable/paths.zig
sed -n '413,460p' src/string/immutable/paths.zigRepository: oven-sh/bun
Length of output: 1299
🏁 Script executed:
#!/bin/bash
# Check what's exported from bun.path module
rg -n "pub.*basename|pub.*fn" --type zig src/bun/path.zig | head -20Repository: oven-sh/bun
Length of output: 111
🏁 Script executed:
#!/by/bash
# Check if bun.path exports basename or similar
rg -n "pub const|pub fn|basename" --type zig src/bun/ | grep -i "path\|basename" | head -30Repository: oven-sh/bun
Length of output: 103
🏁 Script executed:
#!/bin/bash
# Check the actual bun module to see what's available
fd -t f "path.zig" src/bun/ 2>/dev/nullRepository: oven-sh/bun
Length of output: 37
🏁 Script executed:
#!/bin/bash
# Check imports in MetafileBuilder.zig to understand how bun.path is being used
head -50 src/bundler/linker_context/MetafileBuilder.zig | grep -E "^const|^var|@import"Repository: oven-sh/bun
Length of output: 87
🏁 Script executed:
#!/bin/bash
# Find where bun.path is defined/exported
rg -n "pub const path|path.*=" --type zig src/ | grep -v "file path\|file.path\|dir.path" | head -20Repository: oven-sh/bun
Length of output: 1518
🏁 Script executed:
#!/bin/bash
# Check what's in bun.zig (main module file)
fd -t f "bun.zig" src/ | head -5Repository: oven-sh/bun
Length of output: 66
🏁 Script executed:
#!/bin/bash
# Search for path module definition in main bun files
fd -t f "*.zig" src/ -path "*bun*" | grep -E "^src/bun" | head -10Repository: oven-sh/bun
Length of output: 223
🏁 Script executed:
#!/bin/bash
# Check the resolver/resolve_path.zig basename function
sed -n '1747,1760p' src/resolver/resolve_path.zigRepository: oven-sh/bun
Length of output: 446
🏁 Script executed:
#!/bin/bash
# Look for where bun.path is actually aliased/imported
rg -n "bun\.path\." --type zig | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -80 "$(echo {} | cut -d: -f1)" | tail -30'Repository: oven-sh/bun
Length of output: 316
🏁 Script executed:
#!/bin/bash
# Check bun.zig to see if path is exported
head -100 src/bun.zig | grep -E "path|pub const"Repository: oven-sh/bun
Length of output: 853
🏁 Script executed:
#!/bin/bash
# Search for the actual definition of bun.path module
rg -n "pub const path" --type zig src/Repository: oven-sh/bun
Length of output: 793
🏁 Script executed:
#!/bin/bash
# Look for isSepAny function definition to understand where path utilities are
rg -n "pub fn isSepAny|pub const isSepAny" --type zig src/Repository: oven-sh/bun
Length of output: 208
🏁 Script executed:
#!/bin/bash
# Check what module contains isSepAny
rg -n "fn isSepAny" --type zig src/ -B 5 | head -30Repository: oven-sh/bun
Length of output: 624
🏁 Script executed:
#!/bin/bash
# Get more context on target and input_key types
sed -n '500,540p' src/bundler/linker_context/MetafileBuilder.zigRepository: oven-sh/bun
Length of output: 3172
🏁 Script executed:
#!/bin/bash
# Check the full basename function signature in resolve_path.zig
sed -n '1747,1760p' src/resolver/resolve_path.zigRepository: oven-sh/bun
Length of output: 446
🏁 Script executed:
#!/bin/bash
# Check if bun.path.basename is used elsewhere in the codebase
rg -n "bun\.path\.basename" --type zigRepository: oven-sh/bun
Length of output: 588
🏁 Script executed:
#!/bin/bash
# Check isSepAny implementation to confirm it handles both separators
sed -n '1572,1590p' src/resolver/resolve_path.zigRepository: oven-sh/bun
Length of output: 684
Use bun.path.basename for mixed separator handling.
std.fs.path.basename is OS-separator-specific and will fail on paths with mixed separators. Since import specifiers can use / even on Windows, use bun.path.basename (available in src/resolver/resolve_path.zig) which correctly handles both / and \ via isSepAny().
♻️ Refactor
- const target_base = std.fs.path.basename(target);
- const key_base = std.fs.path.basename(input_key);
+ const target_base = bun.path.basename(target);
+ const key_base = bun.path.basename(input_key);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const target_base = std.fs.path.basename(target); | |
| const key_base = std.fs.path.basename(input_key); | |
| const target_base = bun.path.basename(target); | |
| const key_base = bun.path.basename(input_key); |
🤖 Prompt for AI Agents
In `@src/bundler/linker_context/MetafileBuilder.zig` around lines 530 - 531,
Replace OS-specific std.fs.path.basename usage with the path utility that
handles mixed separators: use bun.path.basename for computing target_base and
key_base so import specifiers using '/' on Windows are handled correctly; update
the two calls that compute target_base and key_base (currently calling
std.fs.path.basename) to call bun.path.basename from the resolve_path utility
instead.
|
@alii |
| } | ||
|
|
||
| // Write markdown metafile if requested | ||
| if (ctx.bundler_options.metafile_md.len > 0) { |
There was a problem hiding this comment.
Is this what we do for the regular metafile? Why are there two code paths for writing the metafile to disk?
Metafile paths should be relative to the output directory, like all other output files. Updated tests to use relative paths and expect files in outdir. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/bundler/bundle_v2.zig`:
- Around line 2802-2859: The writeMetafileOutput function currently skips disk
writes when outdir.len == 0 but still appends an OutputFile, producing missing
metafiles and using relative paths; change it so writes always occur: when
outdir.len > 0 keep the existing relative-root_dir logic, but when outdir.len ==
0 treat file_path as an absolute path (open parent dirs from std.fs.cwd() or use
an absolute path API) and perform the same write via
jsc.Node.fs.NodeFS.writeFileWithPathBuffer; ensure the bun.PathString for the
NodeFS call and the OutputFile.output_path use the absolute path when outdir is
empty, and preserve existing error logging (bun.Output.warn) and OutputFile
creation (options.OutputFile.init, .loader/.input_loader selection).
In `@test/bundler/metafile.test.ts`:
- Around line 735-737: The tests are reading metafiles from the CWD
(`Bun.file(`${dir}/meta.md`)`) but the CLI uses `--outdir=dist`, so update all
metafile path expectations in test/bundler/metafile.test.ts to join the test
`dir` with the configured outdir (e.g., `dist`) before checking
existence/reading (replace `${dir}/meta.md`/`${dir}/meta.json` with
`${dir}/dist/meta.md`/`${dir}/dist/meta.json` or derive the outdir variable used
in the test harness), updating every occurrence noted in the comment so the
tests assert the new outdir-relative locations.
| /// Writes a metafile (JSON or markdown) to disk and appends it to the output_files list. | ||
| /// Metafile paths are relative to outdir, like all other output files. | ||
| fn writeMetafileOutput( | ||
| output_files: *std.array_list.Managed(options.OutputFile), | ||
| outdir: []const u8, | ||
| file_path: []const u8, | ||
| content: []const u8, | ||
| output_kind: jsc.API.BuildArtifact.OutputKind, | ||
| ) !void { | ||
| if (outdir.len > 0) { | ||
| // Open the output directory | ||
| var root_dir = std.fs.cwd().makeOpenPath(outdir, .{}) catch |err| { | ||
| bun.Output.warn("Failed to open output directory '{s}': {s}", .{ outdir, @errorName(err) }); | ||
| return; | ||
| }; | ||
| defer root_dir.close(); | ||
|
|
||
| // Create parent directories if needed (relative to outdir) | ||
| if (std.fs.path.dirname(file_path)) |parent| { | ||
| if (parent.len > 0) { | ||
| root_dir.makePath(parent) catch {}; | ||
| } | ||
| } | ||
|
|
||
| // Write to disk relative to outdir | ||
| var path_buf: bun.PathBuffer = undefined; | ||
| _ = jsc.Node.fs.NodeFS.writeFileWithPathBuffer(&path_buf, .{ | ||
| .data = .{ .buffer = .{ | ||
| .buffer = .{ | ||
| .ptr = @constCast(content.ptr), | ||
| .len = @as(u32, @truncate(content.len)), | ||
| .byte_len = @as(u32, @truncate(content.len)), | ||
| }, | ||
| } }, | ||
| .encoding = .buffer, | ||
| .mode = 0o644, | ||
| .dirfd = bun.FD.fromStdDir(root_dir), | ||
| .file = .{ .path = .{ | ||
| .string = bun.PathString.init(file_path), | ||
| } }, | ||
| }).unwrap() catch |err| { | ||
| bun.Output.warn("Failed to write metafile to '{s}': {s}", .{ file_path, @errorName(err) }); | ||
| }; | ||
| } | ||
|
|
||
| // Add as OutputFile so it appears in result.outputs | ||
| const is_json = output_kind == .@"metafile-json"; | ||
| try output_files.append(options.OutputFile.init(.{ | ||
| .loader = if (is_json) .json else .file, | ||
| .input_loader = if (is_json) .json else .file, | ||
| .input_path = bun.handleOom(bun.default_allocator.dupe(u8, if (is_json) "metafile.json" else "metafile.md")), | ||
| .output_path = bun.handleOom(bun.default_allocator.dupe(u8, file_path)), | ||
| .data = .{ .saved = content.len }, | ||
| .output_kind = output_kind, | ||
| .is_executable = false, | ||
| .side = null, | ||
| .entry_point_index = null, | ||
| })); |
There was a problem hiding this comment.
Write metafiles even when outdir is empty and use absolute paths.
Right now, disk writes are skipped when outdir.len == 0, yet an OutputFile is still appended—this yields missing files for in-memory builds that still request metafile_json_path/metafile_markdown_path. Also, file ops here are relative even though src/**/*.zig must use absolute paths.
🔧 Proposed fix
fn writeMetafileOutput(
output_files: *std.array_list.Managed(options.OutputFile),
outdir: []const u8,
file_path: []const u8,
content: []const u8,
output_kind: jsc.API.BuildArtifact.OutputKind,
) !void {
- if (outdir.len > 0) {
- // Open the output directory
- var root_dir = std.fs.cwd().makeOpenPath(outdir, .{}) catch |err| {
- bun.Output.warn("Failed to open output directory '{s}': {s}", .{ outdir, `@errorName`(err) });
- return;
- };
- defer root_dir.close();
-
- // Create parent directories if needed (relative to outdir)
- if (std.fs.path.dirname(file_path)) |parent| {
- if (parent.len > 0) {
- root_dir.makePath(parent) catch {};
- }
- }
-
- // Write to disk relative to outdir
- var path_buf: bun.PathBuffer = undefined;
- _ = jsc.Node.fs.NodeFS.writeFileWithPathBuffer(&path_buf, .{
- .data = .{ .buffer = .{
- .buffer = .{
- .ptr = `@constCast`(content.ptr),
- .len = `@as`(u32, `@truncate`(content.len)),
- .byte_len = `@as`(u32, `@truncate`(content.len)),
- },
- } },
- .encoding = .buffer,
- .mode = 0o644,
- .dirfd = bun.FD.fromStdDir(root_dir),
- .file = .{ .path = .{
- .string = bun.PathString.init(file_path),
- } },
- }).unwrap() catch |err| {
- bun.Output.warn("Failed to write metafile to '{s}': {s}", .{ file_path, `@errorName`(err) });
- };
- }
+ var path_buf: bun.PathBuffer = undefined;
+ const abs_path = if (std.fs.path.isAbsolute(file_path))
+ file_path
+ else
+ bun.path.joinAbsStringBuf(
+ bun.fs.FileSystem.instance.top_level_dir,
+ &path_buf,
+ if (outdir.len > 0) &[_][]const u8{ outdir, file_path } else &[_][]const u8{ file_path },
+ .auto,
+ );
+
+ if (std.fs.path.dirname(abs_path)) |parent| {
+ bun.FD.cwd().makePath(u8, parent) catch {};
+ }
+
+ _ = jsc.Node.fs.NodeFS.writeFileWithPathBuffer(&path_buf, .{
+ .data = .{ .buffer = .{
+ .buffer = .{
+ .ptr = `@constCast`(content.ptr),
+ .len = `@as`(u32, `@truncate`(content.len)),
+ .byte_len = `@as`(u32, `@truncate`(content.len)),
+ },
+ } },
+ .encoding = .buffer,
+ .mode = 0o644,
+ .dirfd = bun.FD.cwd(),
+ .file = .{ .path = .{
+ .string = bun.PathString.init(abs_path),
+ } },
+ }).unwrap() catch |err| {
+ bun.Output.warn("Failed to write metafile to '{s}': {s}", .{ abs_path, `@errorName`(err) });
+ };As per coding guidelines, use absolute paths in file operations.
🤖 Prompt for AI Agents
In `@src/bundler/bundle_v2.zig` around lines 2802 - 2859, The writeMetafileOutput
function currently skips disk writes when outdir.len == 0 but still appends an
OutputFile, producing missing metafiles and using relative paths; change it so
writes always occur: when outdir.len > 0 keep the existing relative-root_dir
logic, but when outdir.len == 0 treat file_path as an absolute path (open parent
dirs from std.fs.cwd() or use an absolute path API) and perform the same write
via jsc.Node.fs.NodeFS.writeFileWithPathBuffer; ensure the bun.PathString for
the NodeFS call and the OutputFile.output_path use the absolute path when outdir
is empty, and preserve existing error logging (bun.Output.warn) and OutputFile
creation (options.OutputFile.init, .loader/.input_loader selection).
| // Check meta.md was created | ||
| const metaFile = Bun.file(`${dir}/meta.md`); | ||
| expect(await metaFile.exists()).toBe(true); |
There was a problem hiding this comment.
Fix CLI metafile path expectations to honor --outdir.
All CLI tests pass --outdir=dist but read meta.md/meta.json from the cwd. With outdir-relative metafiles, these should be under ${dir}/dist/…. This will fail once the new write behavior is active.
🔧 Suggested fix (apply to all CLI meta file reads)
- const metaFile = Bun.file(`${dir}/meta.md`);
+ const metaFile = Bun.file(`${dir}/dist/meta.md`);- const content = await Bun.file(`${dir}/meta.md`).text();
+ const content = await Bun.file(`${dir}/dist/meta.md`).text();- const jsonFile = Bun.file(`${dir}/meta.json`);
- const mdFile = Bun.file(`${dir}/meta.md`);
+ const jsonFile = Bun.file(`${dir}/dist/meta.json`);
+ const mdFile = Bun.file(`${dir}/dist/meta.md`);Also applies to: 770-772, 797-802, 833-833, 861-861, 885-885, 911-911, 938-938, 963-963, 994-994, 1023-1023, 1057-1057, 1082-1082, 1107-1107, 1147-1147, 1172-1172, 1197-1197
🤖 Prompt for AI Agents
In `@test/bundler/metafile.test.ts` around lines 735 - 737, The tests are reading
metafiles from the CWD (`Bun.file(`${dir}/meta.md`)`) but the CLI uses
`--outdir=dist`, so update all metafile path expectations in
test/bundler/metafile.test.ts to join the test `dir` with the configured outdir
(e.g., `dist`) before checking existence/reading (replace
`${dir}/meta.md`/`${dir}/meta.json` with
`${dir}/dist/meta.md`/`${dir}/dist/meta.json` or derive the outdir variable used
in the test harness), updating every occurrence noted in the comment so the
tests assert the new outdir-relative locations.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/bun.js/api/JSBundler.zig (1)
245-262: Tidy metafile field comments.
The “TEST: moved here” note looks leftover, and the metafile comment should reflect Markdown as well.♻️ Suggested comment cleanup
- /// Path to write JSON metafile (if specified via metafile object) - TEST: moved here + /// Path to write JSON metafile (if specified via metafile object) metafile_json_path: OwnedString = OwnedString.initEmpty(bun.default_allocator), - /// Path to write markdown metafile (if specified via metafile object) - TEST: moved here + /// Path to write Markdown metafile (if specified via metafile object) metafile_markdown_path: OwnedString = OwnedString.initEmpty(bun.default_allocator), ... - /// Generate metafile (JSON module graph) + /// Generate metafile (JSON/Markdown module graph) metafile: bool = false,
|
Hey, so I may have found a bug when running using this CLI flag. I can't seem to have the filepath for --metafile and --metafile-md working unless the file itself on the path already exists. For example: Also the same when running |
…lysis (oven-sh#26441) ## Summary - Adds `--metafile-md` CLI option to `bun build` that generates a markdown visualization of the module graph - Designed to help Claude and other LLMs analyze bundle composition, identify bloat, and understand dependency chains - Reuses existing metafile JSON generation code as a post-processing step ## Features The generated markdown includes: 1. **Quick Summary** - Module counts, sizes, ESM/CJS breakdown, output/input ratio 2. **Largest Input Files** - Sorted by size to identify potential bloat 3. **Entry Point Analysis** - Shows bundle size, exports, CSS bundles, and bundled modules 4. **Dependency Chains** - Most commonly imported modules and reverse dependencies 5. **Full Module Graph** - Complete import/export info for each module 6. **Raw Data for Searching** - Grep-friendly markers in code blocks: - `[MODULE:]`, `[SIZE:]`, `[IMPORT:]`, `[IMPORTED_BY:]` - `[ENTRY:]`, `[EXTERNAL:]`, `[NODE_MODULES:]` ## Usage ```bash # Default filename (meta.md) bun build entry.js --metafile-md --outdir=dist # Custom filename bun build entry.js --metafile-md=analysis.md --outdir=dist # Both JSON and markdown bun build entry.js --metafile=meta.json --metafile-md=meta.md --outdir=dist ``` ## Example Output See sample output: https://gist.github.com/example (will add) ## Test plan - [x] Test default filename (`meta.md`) - [x] Test custom filename - [x] Test both `--metafile` and `--metafile-md` together - [x] Test summary metrics - [x] Test module format info (ESM/CJS) - [x] Test external imports - [x] Test exports list - [x] Test bundled modules table - [x] Test CSS bundle reference - [x] Test import kinds (static, dynamic, require) - [x] Test commonly imported modules - [x] Test largest files sorting (bloat analysis) - [x] Test output/input ratio - [x] Test grep-friendly raw data section - [x] Test entry point markers - [x] Test external import markers - [x] Test node_modules markers All 17 new tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
…c_sys, and orphaned files (#38213) Scheduled dead-code sweep. Areas were chosen to avoid the 17 dead-code PRs currently open (#35437 through #38005): every file below was either untouched by those PRs or, where a file is shared, the hunks are different symbols (checked mechanically against the open PR diffs; the only shared removed lines are `level = "expect"` style TOML boilerplate in unrelated `hawk.toml` blocks). Net: 47 files, about 2.7k lines removed; the only additions are the guard test, three one-line `#[cfg]` / import adjustments, and two doc-comment fixes for functions that went away. ### Removed **WebCore streams bindings (transferable streams were never implemented)** - `m_detached` bitfields on `JSReadableStream`, `JSWritableStream`, `JSTransformStream`: never read or written. - `$bunNativeType` / `$disturbed` private-name accessors on `ReadableStream.prototype` (4 getters/setters, their installs, `m_nativeType`, the reset in `ReadableStream__detach`, the two `BunBuiltinNames.h` entries and `builtins.d.ts` declarations): no builtin, C++ or test referenced either name. `$bunNativePtr` is still used and stays. **TextEncoder** - `TextEncoder::encode` / `encodeInto` / `EncodeIntoResult` and the `convertDictionary<EncodeIntoResult>` / `convertDictionaryToJS` specializations (`TextEncoder.h`, `TextEncoder.cpp`, `JSTextEncoder.h`, `JSTextEncoder.cpp`): the wrapper implements both methods through the Rust `TextEncoder__encode*` exports and only ever calls `impl.encoding()`, which is all that is left of the impl class. **node:crypto** - `JSKeyObject::create` and `JSKeyObject::subspaceFor` plus the `m_subspaceForJSKeyObject` / `m_clientSubspaceForJSKeyObject` slots: every key object is one of the three final subclasses, each of which defines its own `create`/`subspaceFor`; the base class is only used as a downcast target. - Commented-out `switch (m_curve)` block in `CryptoKeyOKP::algorithm()` (2023). **Other bindings** - `WriteBarrierList::list()`, the `WeakRefFinalizeFn` typedef in `Weak.cpp`, `JSC_MAC_VERSION_TBA` / `JSC_IOS_VERSION_TBA` in `root.h` (unused by every JSC header in the WebKit builds we ship against). - `Bun__resolve` host export (`bun_resolve` in `BunObject.rs` + the `extern "C"` declaration in `ImportMetaObject.h`): nothing in C++ or JS called it; the `Bun__resolveSync*` family is what is used. **node:http (`src/js/node/_http_server.ts`)** - `kDeprecatedReplySymbol` is a module-private `Symbol()` in `internal/http` that nothing ever sets on a response's options, so the constructor branch that installed the fetch-`Response` based `write`/`end` was unreachable. Removed it together with everything only it reached: `ServerResponse_writeDeprecated`, `ServerResponse_finalDeprecated`, `ensureReadableStreamController`, `drainHeadersIfObservable`, `emitRequestCloseNT`, `GlobalPromise`, and the now-unused imports (`controllerSymbol`, `firstWriteSymbol`, `deferredSymbol`, `runSymbol`, `emitErrorNextTickIfErrorListenerNT`). This also stops adding an `undefined`-valued `Symbol(deprecatedReply)` own property to every `ServerResponse`. - The `isNextIncomingMessageHTTPS` save/set/restore around request dispatch: the flag's only reader was removed in c4a937c, so the calls had no effect. - A 2025 commented-out `cluster._getServer` block in `Server.prototype.listen`. **Rust** (cross-crate analysis with hawk per `tools/hawk/README.md`, then each item re-checked with `rg`; callers were confirmed to be platform-gated, e.g. `node_fs` uses `sys_uv` on Windows) - `bun_sys`: `link`, `fdatasync` and the non-Linux `sendfile` stub (both the posix and Windows arms), the Windows arms of `fchown`, `chmod`, `chown`, `fsync`, `linkat`, `fchmodat`, `lchmod`, `lchown`, `futimens`, `lutimens`, `fcntl`, `socketpair`, the Windows `Name::as_zstr`, `c::kqueue` / `c::kevent` / `c::fork` / `c::fd_t`, `linux::Errno`, `darwin::OSLog::as_ptr`, the non-macOS `clonefile` stub, the non-Windows `get_fd_path_w` stub, `posix::sysctlbyname` (the typed `sysctl_read*` helpers stay), `posix::write`; and the two helpers that became unreferenced as a result, `linux_syscall::write_raw` and the `safe_libc::fdatasync` import, plus `windows::timespec_to_filetime`. `Tag::futimens` is now `#[cfg(not(windows))]` like the other tags whose only users are posix-side (the Windows `cargo check` flagged it once its Windows user was gone). - `bun_lsquic_sys`: the `Engine` wrapper (struct, impl, `Drop`), `Conn::{raw, set_ctx, ctx, n_avail_streams, sockaddr, status}`, `global_init`, `enable_logging`, `LSQVER_I001/I002`, and the `lsquic_conn_n_avail_streams` extern. `node:quic` drives lsquic through the raw externs directly. - `bun_spawn_sys`: `PosixSpawnResult::close` and the non-Linux `pifd_from_pid` stub (the only call site is Linux-gated). The `FdExt` import that `close` was the last Windows user of is now `#[cfg(unix)]`. - `bun_tcc_sys`: `State::run` and the `tcc_run` extern. - `hawk.toml`: the six `bun_platform` `darwin::Category::*` overrides, whose variants were deleted in #36833 (hawk reports them as `unknown_item`). **Orphaned files** (zero references repo-wide, searched with `git grep` including `.github`, `.buildkite`, `.vscode`, `scripts/`, `packages/` and the generated `build/debug/codegen/`) - `misctools/gdb/std_gdb_pretty_printers.py` (Zig standard-library pretty printers; the repo has no Zig left) and the `.vscode/launch.json` line that sourced it; `misctools/mime.js` (emitted a Zig `ComptimeStringMap`; MIME types now come from `src/http_types/mime_type_list.txt`); `misctools/.gitignore` (ignored outputs of Zig programs deleted long ago). - `patches/ncrypto.patch`: a one-off diff against Node's ncrypto committed with #17692. Unlike every other file under `patches/`, no `scripts/build/deps/*.ts` applies it, and `ncrypto.cpp`/`.h` have changed many times since, so it no longer describes anything. - `meta.json` (stray `--metafile` output committed in #26441), `workspace.code-workspace` (2021 single-folder VS Code workspace with Zig settings; `.vscode/` is the live config). - `src/jsc/bindings/v8-capture-stack-fixture.cjs`, `src/jsc/bindings/webcore/EventNames.in` (WebKit `make_event_factory.pl` input; Bun's `EventNames.h` is hand-written), `src/runtime/ffi/libtcc1.a.macos-aarch64` (superseded by the embedded `libtcc1.c`). - `src/runtime/bake/client/JavaScriptSyntaxHighlighterComponent.tsx` (its header says the client never uses it) and `JavaScriptSyntaxHighlighter.css`, which only it imported. The live `JavaScriptSyntaxHighlighter.ts` is untouched. - `packages/bun-release/scripts/npm-exec.ts` (`upload-npm.ts` bundles only `npm-postinstall.ts` and ships placeholder bins), `packages/bun-usockets/misc/{manual.md,gen_test_certs.sh,layout.png}` and `packages/bun-usockets/module.modulemap` (upstream leftovers, same class as the `bun-uws/misc` files removed in #37659). ### Verification - `rg` / `git grep` for every symbol and file name above across `src/`, `scripts/`, `packages/`, `test/`, `vendor/WebKit/Source` (for the C++ symbols) and freshly regenerated `build/debug/codegen/`. - Rust items come from a hawk `dead_public` report on this tree (release profile, all 11 shipped targets), filtered to items no open PR deletes; findings that are FFI struct fields, code tables, or API added in the last week were deliberately left alone. - `bun bd` builds, and the removed symbols are absent from the resulting binary / bundled JS / `generated_host_exports.rs`. `bun bd test` passes on `test/js/web/streams/streams.test.js`, `test/js/web/encoding/text-encoder.test.js`, `test/js/node/crypto/crypto.key-objects.test.ts`, `test/js/node/fs/fs.test.ts`, `test/js/bun/resolve/import-meta*.test.*` and `test/js/node/quic/quic-stream.test.ts`; `test/js/node/http/node-http.test.ts` passes except "request via http proxy, issue#4295", which fails identically with an unmodified bun in this container (ECONNREFUSED to its local proxy). - `cargo check --workspace` on the windows-msvc, darwin, freebsd, linux-musl, android and linux-gnu targets. - `test/internal/source-lints/dead-symbols-streams-http-misctools.test.ts` pins everything above; all 40 content checks and 16 deleted-file checks fail against main and pass here. ### Left alone (probably dead, not deleted) - `src/simdutf_sys/simdutf.rs`: the whole `utf32` / big-endian wrapper tree (~150 lines, plus its externs and the matching shims in `bun-simdutf.cpp`) has no callers, but #37332 is editing the same extern block; worth a follow-up once that lands. Same story for the unused `Loop` / `uv_stat_t` / `ReturnCode` helpers in `libuv_sys`. - `src/js/internal/http.ts`: `kDeprecatedReplySymbol`, `controllerSymbol`, `runSymbol`, `deferredSymbol`, `firstWriteSymbol` and `get/setIsNextIncomingMessageHTTPS` lost their last users in this PR, but #35437 rewrites that exact region of the file. - The rest of the transferable-streams scaffolding: `JSCrossRealmTransformState` (never created; its only references are its `FOR_EACH_WEB_STREAMS_INTERNAL_STRUCTURE` entry and iso-subspace slots), `CrossRealmTransform.cpp`, and the `SourceKind::CrossRealm` / `SinkKind::CrossRealm` arms with their `case` labels. #37332 is already editing that cluster, so it is best removed as one unit once that lands. - `misctools/gen-unicode-table.ts` + `unicode-generator.ts` emit Zig source, but `src/bun_core/string/identifier.rs` still points at them as the generator to port; `misctools/generate-cli-completions.ts` + `completions/bun-cli.json` and `completions/spec.yaml` have no in-repo consumers but may have external ones. - `src/runtime/bake/{incremental,memory}_visualizer.html` (~800 lines): nothing serves them since the port, but `DevServer` still carries the message writers and stubs, so this looks like an unfinished port rather than dead code. - `packages/bun-inspector-frontend` (build script points at a path that no longer exists) and `packages/bun-build-mdx-rs` (2024 proof of concept) are unreferenced but are a product call. - `bun_shim_impl::read_without_launch` is the Windows `bunx` fast path that nothing calls any more; deleting it would drop a feature rather than a leftover. <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 1 · 47 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 3 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-streams-http-misctools.test.ts bun test v1.4.0 (59bf369) test/internal/source-lints/dead-symbols-streams-http-misctools.test.ts: 71 | // The Bun__resolve host export had no C++ or JS caller (only Bun__resolveSync 72 | // and its variants are used). 73 | ["src/jsc/bindings/ImportMetaObject.h", /\bBun__resolve\(/], 74 | ["src/runtime/api/BunObject.rs", /HOST_EXPORT\(Bun__resolve,|\bfn bun_resolve\b/], 75 | ]), 76 | ).toEqual([]); ^ error: expect(received).toEqual(expected) - [] + [ + "src/jsc/bindings/webcore/streams/JSReadableStream.h: \bm_detached\b|\bm_nativeType\b", + "src/jsc/bindings/webcore/streams/JSTransformStream.h: \bm_detached\b", + "src/jsc/bindings/webcore/streams/JSWritableStream.h: \bm_detached\b", + "src/jsc/bindings/webcore/streams/JSReadableStream.cpp: bunNativeTypePrivateName|disturbedPrivateName", + "src/js/builtins/BunBuiltinNames.h: macro\((bunNativeType|disturbed)\)", + "src/jsc/bindings/webcore/TextEncoder.h: EncodeInto ... (truncated) release without fix: 3 FAILED bun test v1.4.0-canary.1 (da3851e) test/internal/source-lints/dead-symbols-streams-http-misctools.test.ts: 71 | // The Bun__resolve host export had no C++ or JS caller (only Bun__resolveSync 72 | // and its variants are used). 73 | ["src/jsc/bindings/ImportMetaObject.h", /\bBun__resolve\(/], 74 | ["src/runtime/api/BunObject.rs", /HOST_EXPORT\(Bun__resolve,|\bfn bun_resolve\b/], 75 | ]), 76 | ).toEqual([]); ^ error: expect(received).toEqual(expected) - [] + [ + "src/jsc/bindings/webcore/streams/JSReadableStream.h: \bm_detached\b|\bm_nativeType\b", + "src/jsc/bindings/webcore/streams/JSTransformStream.h: \bm_detached\b", + "src/jsc/bindings/webcore/streams/JSWritableStream.h: \bm_detached\b", + "src/jsc/bindings/webcore/streams/JSReadableStream.cpp: bunNativeTypePrivateName|disturbedPrivateName", + "src/js/builtins/BunBuiltinNames.h: macro\((bunNativeType|disturbed)\)", + "src/jsc/bindings/webcore/TextEncoder.h: EncodeIntoResult|\bencodeInto\b", + "src/jsc/bindings/webcore/TextEncoder.cpp: TextEncoder::encode(Into)?\(", + "src/jsc/bindings/webcore/JSTextEncoder.h: EncodeIntoResult", + "src/jsc/bindings/webco ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-streams-http-misctools.test.ts bun test v1.4.0 (59bf369) test/internal/source-lints/dead-symbols-streams-http-misctools.test.ts: (pass) dead stream slots and other dead C++ bindings do not reappear [33.31ms] (pass) the deprecated-reply ServerResponse path stays out of node:http [21.74ms] (pass) dead Rust wrappers do not reappear [63.04ms] (pass) orphaned files stay deleted [545.87ms] 4 pass 0 fail 4 expect() calls Ran 4 tests across 1 file. [3.00s] __F:0:S:0 release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 662ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/130] gen bake.{client,server,error}.js -> bake.client.js, bake.server.js, bake.error.js [2/130] gen generated_host_exports.rs generated_host_exports.rs: 92 exports (host=3, lazy=10, generic=79, rust=0); 239 extern-C blocks audited [3/130] gen cpp.rs (cppbind) [4/130] gen JS modules (bundle-modules) Preprocess modules (9626ms) Bundle modules (50ms) Postprocesss modules (239ms) Bundle Functions (777ms) Generate Code (34ms) [10.75s] Bundled "src/js" for production 2622 kb 197 internal modules 13 native modules 91 internal functions across 17 files [4/129] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu) nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19) �[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno) �[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr) �[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` .vscode/launch.json | 1 - hawk.toml | 48 -- meta.json | 24 - misctools/.gitignore | 10 - misctools/gdb/std_gdb_pretty_printers.py | 142 ---- misctools/mime.js | 46 -- packages/bun-release/scripts/npm-exec.ts | 13 - packages/bun-usockets/misc/gen_test_certs.sh | 51 -- packages/bun-usockets/misc/layout.png | Bin 10991 -> 0 bytes packages/bun-usockets/misc/manual.md | 180 ---- packages/bun-usockets/module.modulemap | 4 - patches/ncrypto.patch | 919 --------------------- src/js/builtins.d.ts | 2 - src/js/builtins/BunBuiltinNames.h | 2 - src/js/node/_http_server.ts | 209 ----- src/jsc/bindings/ImportMetaObject.h | 1 - src/jsc/bindings/Weak.cpp | 2 - src/jsc/bindings/WriteBarrierList.h | 5 - src/jsc/bindings/node/crypto/JSKeyObject.h | 20 - src/jsc/bindings/root.h | 3 - src/jsc/bindings/v8-capture-stack-fixture.cjs | 15 - src/jsc/bindings/webcore/DOMClientIsoSubspaces.h | 1 - src/jsc/bindings/webcore/DOMIsoSubspaces.h | 1 - src/jsc/bindings/webcore/EventNames.in | 101 --- src/jsc/bindings/webcore/JSTextEncoder.cpp | 56 -- src/jsc/bindings/webcore/JSTextEncoder.h | 4 - src/jsc/bindings/webcore/TextEncoder.cpp | 36 - src/jsc/bindings/webcore/TextEncoder.h | 9 - .../bindings/webcore/streams/JSReadableStream.cpp | 60 +- .../bindings/webcore/streams/JSReadableStream.h | 4 - .../bindings/webcore/streams/JSTransformStream.h | 2 - .../bindings/webcore/streams/JSWritableStream.h | 2 - .../bindings ... (truncated) ``` </details> **gate history** · 1 passed · 1 rejected · iteration 1 <details><summary>evidence per changed file</summary> ``` file reads edits tests .vscode/launch.json 0 0 0 hawk.toml 0 0 0 meta.json 0 0 0 misctools/.gitignore 0 0 0 misctools/gdb/std_gdb_pretty_printers.py 0 0 0 misctools/mime.js 0 0 0 packages/bun-release/scripts/npm-exec.ts 0 0 0 packages/bun-usockets/misc/gen_test_certs.sh 0 0 0 packages/bun-usockets/misc/layout.png 0 0 0 packages/bun-usockets/misc/manual.md 0 0 0 packages/bun-usockets/module.modulemap 0 0 0 patches/ncrypto.patch 0 0 0 src/js/builtins.d.ts 0 0 0 src/js/builtins/BunBuiltinNames.h 0 0 0 src/js/node/_http_server.ts 5 0 0 src/jsc/bindings/ImportMetaObject.h 0 0 0 (+ 31 more files) ``` </details> <!-- robobun:evidence:end --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Summary
--metafile-mdCLI option tobun buildthat generates a markdown visualization of the module graphFeatures
The generated markdown includes:
[MODULE:],[SIZE:],[IMPORT:],[IMPORTED_BY:][ENTRY:],[EXTERNAL:],[NODE_MODULES:]Usage
Example Output
See sample output: https://gist.github.com/example (will add)
Test plan
meta.md)--metafileand--metafile-mdtogetherAll 17 new tests pass.
🤖 Generated with Claude Code