Skip to content

feat(build): add --metafile-md CLI option for LLM-friendly bundle analysis - #26441

Merged
dylan-conway merged 10 commits into
mainfrom
claude/metafile-md-cli-option
Jan 29, 2026
Merged

dylan-conway merged 10 commits into
mainfrom
claude/metafile-md-cli-option

Conversation

@robobun

@robobun robobun commented Jan 25, 2026

Copy link
Copy Markdown
Collaborator

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

# 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

  • Test default filename (meta.md)
  • Test custom filename
  • Test both --metafile and --metafile-md together
  • Test summary metrics
  • Test module format info (ESM/CJS)
  • Test external imports
  • Test exports list
  • Test bundled modules table
  • Test CSS bundle reference
  • Test import kinds (static, dynamic, require)
  • Test commonly imported modules
  • Test largest files sorting (bloat analysis)
  • Test output/input ratio
  • Test grep-friendly raw data section
  • Test entry point markers
  • Test external import markers
  • Test node_modules markers

All 17 new tests pass.

🤖 Generated with Claude Code

…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>
@robobun

robobun commented Jan 25, 2026 •

Copy link
Copy Markdown
Collaborator Author

@coderabbitai

coderabbitai Bot commented Jan 25, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds 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

Cohort / File(s) Summary
Metafile markdown generator
src/bundler/linker_context/MetafileBuilder.zig
Added pub fn generateMarkdown(allocator: std.mem.Allocator, metafile_json: []const u8) ![]const u8 to parse metafile JSON and emit a multi‑section Markdown document; added helper fn stripParentRefs(path: []const u8) []const u8.
CLI options & build flow
src/cli.zig, src/cli/Arguments.zig, src/cli/build_command.zig
Added metafile_md to BundlerOptions; introduced --metafile-md <STR>? (defaults to meta.md if no value); integrated Markdown generation and file write in the build command; added metafile-markdown output kind for summaries.
Bundle/linker options & outputs
src/bundler/LinkerContext.zig, src/bundler/bundle_v2.zig, src/options.zig
Exposed metafile_json_path and metafile_markdown_path on public options; propagated paths through BundleV2 and BuildResult (added metafile_markdown); updated deinit and C ABI Bun__setupLazyMetafile to accept JSON and Markdown strings.
JS API, runtime & bindings
src/bun.js/api/JSBundler.zig, src/bun.js/bindings/BundlerMetafile.cpp, src/js/builtins/BunBuiltinNames.h
Added metafile_json_path and metafile_markdown_path fields to JSBundler/Config; expanded Bun__setupLazyMetafile signature to accept separate JSON and Markdown inputs; lazy metafile getter now reads from metafileJson backing; added builtin identifier metafileJson.
Tests & fixtures
test/bundler/metafile.test.ts, meta.json
Updated tests to use typed (result.metafile as Metafile).inputs/outputs; added comprehensive suite covering JSON and Markdown metafile variants and CLI --metafile-md behaviors; updated/added meta.json fixture.
Build pipeline / server
src/bake/production.zig
Added no‑op handling for metafile-json and metafile-markdown output kinds to exclude them from production module‑map processing.

Possibly related PRs

Suggested reviewers

  • alii
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature: adding a --metafile-md CLI option for markdown bundle analysis, which aligns with the primary changeset.
Description check ✅ Passed The description comprehensively covers the PR template sections with clear summary, features, usage examples, test plan checklist, and detailed output specifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Comment on lines +398 to +404
const InputFileInfo = struct {
path: []const u8,
bytes: u64,
import_count: u32,
is_node_modules: bool,
format: []const u8,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment on lines +486 to +505
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the file size and read the relevant sections
wc -l src/bundler/linker_context/MetafileBuilder.zig

Repository: 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.zig

Repository: 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.zig

Repository: oven-sh/bun

Length of output: 631


🏁 Script executed:

# Search for stripParentRefs function
rg -n "stripParentRefs" src/bundler/linker_context/MetafileBuilder.zig -A 10

Repository: 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 5

Repository: 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.zig

Repository: 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 -20

Repository: 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 -20

Repository: 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 zig

Repository: 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 -5

Repository: 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 -5

Repository: 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 -40

Repository: 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.

Comment on lines +857 to +860
"entry.js": `import "./small.js"; import "./large.js";`,
"small.js": `export const s = 1;`,
"large.js": `export const large = "${"x".repeat(500)}";`,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Suggested change
"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().

Claude Bot and others added 2 commits January 26, 2026 01:50
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and PathOnly are 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 /, and stripParentRefs only strips ../ and ./, so imports using \ won’t match on Windows. Consider bun.path.isSepAny and 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: Use Buffer.alloc(...).toString() for the large test string.

Replace .repeat(500) with Buffer.alloc(500, "x").toString() to align with test guidelines. As per coding guidelines, prefer Buffer.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()}";`,

Comment on lines +416 to +421
const gop = try bytes_in_output.getOrPut(module_path);
if (gop.found_existing) {
gop.value_ptr.* += bytes_val;
} else {
gop.value_ptr.* = bytes_val;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/bundler/bundle_v2.zig Outdated
Comment on lines 166 to 170
macro(makeGetterTypeError) \
macro(maxAge) \
macro(method) \
macro(metafileJson) \
macro(mockedFunction) \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +731 to +734
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(exitCode).toBe(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/bundler/bundle_v2.zig
Comment on lines +2782 to +2871
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,
}));
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread src/cli/build_command.zig
Comment on lines +356 to +383
// 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;
},
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
// 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.

@alii

alii commented Jan 26, 2026

Copy link
Copy Markdown
Member

Can we also add to the programmatic Bun.build() API (maybe Bun.build({metafileMd: ..}))

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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +530 to +531
const target_base = std.fs.path.basename(target);
const key_base = std.fs.path.basename(input_key);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 -30

Repository: 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 -5

Repository: 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.zig

Repository: 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 3

Repository: 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 path

Repository: 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.zig

Repository: 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 -20

Repository: 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 -30

Repository: 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/null

Repository: 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 -20

Repository: 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 -5

Repository: 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 -10

Repository: 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.zig

Repository: 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 -30

Repository: 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.zig

Repository: 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.zig

Repository: 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 zig

Repository: 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.zig

Repository: 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.

Suggested change
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.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@alii Bun.build({metafile: { markdown: true } ..})

Comment thread src/cli/build_command.zig
}

// Write markdown metafile if requested
if (ctx.bundler_options.metafile_md.len > 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/bundler/bundle_v2.zig
Comment on lines +2802 to +2859
/// 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,
}));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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).

Comment on lines +735 to +737
// Check meta.md was created
const metaFile = Bun.file(`${dir}/meta.md`);
expect(await metaFile.exists()).toBe(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

@dylan-conway
dylan-conway merged commit aded701 into main Jan 29, 2026
29 of 43 checks passed
@dylan-conway
dylan-conway deleted the claude/metafile-md-cli-option branch January 29, 2026 02:01
@autorejecttop

Copy link
Copy Markdown

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: bun build ./src/index.ts --outdir ./dist --metafile ./dist/meta.json just like in the docs. It always returns

error: ModuleNotFound resolving "./dist/meta.json" (entry point)

Also the same when running bun build ./src/index.ts --outdir ./dist --metafile-md ./dist/meta.md just like in the docs. It always returns

error: ModuleNotFound resolving "./dist/meta.md" (entry point)

xhjkl pushed a commit to xhjkl/bun that referenced this pull request May 14, 2026
…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>
Jarred-Sumner added a commit that referenced this pull request Aug 14, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants