Skip to content

fix(bundler): collect external imports from converted stmts for bytecode ModuleInfo#27456

Closed
robobun wants to merge 2 commits into
mainfrom
claude/fix-bytecode-esm-module-info-27454
Closed

fix(bundler): collect external imports from converted stmts for bytecode ModuleInfo#27456
robobun wants to merge 2 commits into
mainfrom
claude/fix-bytecode-esm-module-info-27454

Conversation

@robobun

@robobun robobun commented Feb 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fixes bun build --compile --bytecode --minify --format esm failing with "Cannot find module" errors when using named imports from npm packages (e.g., import { createTwoFilesPatch } from 'diff')
  • The root cause was postProcessJSChunk scanning original AST statements for external imports and relying on record.source_index.isValid() which may not be patched for transitive dependencies resolved through the async pipeline
  • The fix extends DeclCollector to capture s_import statements from converted output (where bundled imports are already removed), then uses these in postProcessJSChunk instead of the original AST

Test plan

  • Added PackageReExportNamedImport test scenario to test/bundler/bundler_compile.test.ts (tests both default and minified variants)
  • All 55 existing bundler_compile.test.ts tests pass
  • All 3 bundler_compile_splitting.test.ts tests pass
  • Verified the original reproduction from 1.3.10 - build output with bytecode+minify+esm fails to run #27454 works correctly

Closes #27454

🤖 Generated with Claude Code

@robobun

robobun commented Feb 26, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:50 PM PT - Mar 9th, 2026

@claude, your commit 89c9ba4 has 7 failures in Build #39096 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 27456

That installs a local version of the PR into your bun-27456 executable, so you can run:

bun-27456 --bun

@coderabbitai

coderabbitai Bot commented Feb 26, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: af311bee-e7b9-4844-a2c6-450af1b8908c

📥 Commits

Reviewing files that changed from the base of the PR and between 7cf2d8e and 89c9ba4.

📒 Files selected for processing (1)
  • src/http.zig

Walkthrough

Collects external import records from converted statements during parallel printing: adds ImportInfoCollected, threads collected imports through DeclCollector and CompileResult, updates post-processing to build ModuleInfo from compile results (avoiding phantom imports), and adds a regression test; minor TLS hostname getter fix.

Changes

Cohort / File(s) Summary
Import metadata structures
src/bundler/bundle_v2.zig
Add ImportInfoCollected (fields: import_record_index: u32, source_index: u32) and an imports array to the javascript variant of CompileResult to carry collected import metadata.
Declaration collection
src/bundler/linker_context/generateCodeForFileInChunkJS.zig, src/bundler/linker_context/generateCompileResultForJSChunk.zig
Extend DeclCollector with imports (ArrayListUnmanaged of CompileResult.ImportInfoCollected) and source_index; record .s_import occurrences into imports; initialize source_index and populate CompileResult.imports when collect_decls is enabled.
Import post-processing refactor
src/bundler/linker_context/postProcessJSChunk.zig
Replace per-part AST scanning with iteration over chunk.compile_results_for_chunk to resolve import records using collected metadata; validate bounds and skip disabled records; locate original s_import by import_record_index to reconstruct default/named/namespace bindings; derive import_meta from part flags; reorder ModuleInfo population to add collected decls, imports, then wrapper declarations.
Tests
test/bundler/bundler_compile.test.ts
Add PackageReExportNamedImport ESM+bytecode test case covering a package re-export scenario to ensure ModuleInfo does not record phantom imports when some AST import records have unresolved source_index.
HTTP TLS hostname getter
src/http.zig
Change getTlsHostname to access tls_props via props.get().server_name (uses getter) instead of direct field access.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive One unrelated change detected: src/http.zig modification to fix a build failure from PR #27838 (tls_props dereference). This is a rebase-time fix necessary for CI to pass, not part of the core import collection fix. The http.zig change appears necessary for build compatibility but is tangential to the primary #27454 fix. Consider separating rebase-time fixes into a separate commit for clarity.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: collecting external imports from converted statements (not original AST) for bytecode ModuleInfo generation.
Description check ✅ Passed The PR description includes both required sections: 'Summary' explaining the fix and root cause, and 'Test plan' detailing verification steps and test additions.
Linked Issues check ✅ Passed The changes fully address #27454's requirements: fixing named imports from npm packages in bytecode+minify+esm builds by collecting imports from converted statements instead of scanning the original AST.

✏️ 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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bundler/linker_context/postProcessJSChunk.zig`:
- Around line 147-155: The code indexes arrays using import_info.source_index
without bounds checks; before using import_info.source_index to access
all_import_records, source_import_records, and c.graph.ast.items(.parts) ensure
the index is within each container's .len (e.g., check import_info.source_index
< all_import_records.len and < c.graph.ast.items(.parts).len) and skip/continue
if out of range, and apply the same guard for the other similar block around the
code referenced at lines ~193-197 to prevent OOB access when metadata is stale.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 30e609e and 5740da8.

📒 Files selected for processing (5)
  • src/bundler/bundle_v2.zig
  • src/bundler/linker_context/generateCodeForFileInChunkJS.zig
  • src/bundler/linker_context/generateCompileResultForJSChunk.zig
  • src/bundler/linker_context/postProcessJSChunk.zig
  • test/bundler/bundler_compile.test.ts

Comment thread src/bundler/linker_context/postProcessJSChunk.zig

@claude claude 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.

No bugs found, but this restructures how external imports are collected for bytecode ModuleInfo in the linker — core bundler plumbing that warrants human review.

Extended reasoning...

Overview

This PR fixes a bug where bun build --compile --bytecode --minify --format esm fails with "Cannot find module" errors for named imports from npm packages with re-export index files. The fix touches 4 source files in src/bundler/linker_context/ and adds a test scenario.

The core change: instead of scanning the original AST statements in postProcessJSChunk to find external imports (where source_index.isValid() was unreliable for transitive async-resolved dependencies), the code now extends DeclCollector to capture s_import statements from the converted output (after convertStmtsForChunk removes bundled imports). These collected imports are threaded through CompileResult to postProcessJSChunk.

Security risks

No security concerns — this is internal bundler plumbing that affects which module metadata is recorded in bytecode caches. No user input handling, auth, or external communication changes.

Level of scrutiny

High scrutiny warranted. This is production-critical bundler linker code that affects all --compile --bytecode ESM builds. The change restructures a key data flow (how external imports are identified for ModuleInfo), introducing a new struct (ImportInfoCollected), adding a field to DeclCollector and CompileResult.javascript, and rewriting the import collection loop in postProcessJSChunk.

The logic appears correct: convertStmtsForChunk calls shouldRemoveImportExportStmt which removes bundled imports, so only truly-external s_import statements survive to be collected by DeclCollector. The implicit skip of CJS-wrapped modules is also correct (their imports are inside wrapper closures, not in out_stmts). The O(parts×stmts) lookup to find the original s_import by import_record_index is acceptable since it only runs for external imports in bytecode+ESM+compile mode.

Other factors

  • The PR is labeled claude (AI-generated), which increases the value of human review for subtle correctness issues.
  • All files are CODEOWNER-protected.
  • A test is included (PackageReExportNamedImport) that reproduces the exact scenario.
  • The PR description mentions all existing tests pass.

@alii

alii commented Mar 9, 2026

Copy link
Copy Markdown
Member

@robobun Rebase main and get CI green

@robobun

robobun commented Mar 9, 2026

Copy link
Copy Markdown
Collaborator Author

Rebase done on current main. Fixed build error (src/http.zig — SSLConfig SharedPtr dereference missing from PR #27838). Pushed 89c9ba4. CI building again — waiting for results...

…ode ModuleInfo

When building with `--compile --bytecode --minify --format esm`, named
imports from npm packages caused "Cannot find module" errors at runtime.

The root cause was in `postProcessJSChunk` section 2 which scanned
original AST statements to collect external imports for ModuleInfo. It
relied on `record.source_index.isValid()` to skip bundled imports, but
the async resolution pipeline may not patch `source_index` on import
records from transitive dependencies. This caused fully-inlined bundled
imports to be incorrectly recorded as external dependencies in
ModuleInfo, creating a mismatch with what JSC's parser sees in the
actual emitted code.

The fix extends DeclCollector to also capture `s_import` statements from
the **converted** output (after `convertStmtsForChunk`). Since bundled
imports are removed during conversion, only truly-external imports
survive. `postProcessJSChunk` now uses these collected imports instead of
scanning the original AST, ensuring ModuleInfo accurately reflects the
emitted code.

Closes #27454

Co-Authored-By: Claude <noreply@anthropic.com>
@robobun robobun force-pushed the claude/fix-bytecode-esm-module-info-27454 branch from 5740da8 to 7cf2d8e Compare March 9, 2026 20:26

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/bundler/linker_context/postProcessJSChunk.zig (1)

202-218: ⚠️ Potential issue | 🟡 Minor

Stop re-adding wrapper refs after collecting final output decls.

js.decls now comes from final out_stmts, so wrapped files already contribute var init_* = __esm(...) / var require_* = __commonJS(...) in step 1. This pass adds the same binding a second time to ModuleInfo.

🛠️ Suggested fix
-        // 3. Add wrapper-generated declarations (init_xxx, require_xxx) that are
-        // not in any part statement.
-        const all_flags = c.graph.meta.items(.flags);
-        const all_wrapper_refs = c.graph.ast.items(.wrapper_ref);
-        for (chunk.content.javascript.parts_in_chunk_in_order) |part_range| {
-            const source_index = part_range.source_index.get();
-            if (all_flags[source_index].wrap != .none) {
-                const wrapper_ref = all_wrapper_refs[source_index];
-                if (!wrapper_ref.isEmpty()) {
-                    const name = chunk.renamer.nameForSymbol(wrapper_ref);
-                    if (name.len > 0) {
-                        const string_id = mi.str(name) catch continue;
-                        mi.addVar(string_id, .declared) catch continue;
-                    }
-                }
-            }
-        }
+        // 3. Wrapper-generated declarations are already present in `js.decls`
+        // because DeclCollector now runs on final `out_stmts`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bundler/linker_context/postProcessJSChunk.zig` around lines 202 - 218,
The loop that re-adds wrapper refs (for
chunk.content.javascript.parts_in_chunk_in_order using
all_flags/all_wrapper_refs and calling chunk.renamer.nameForSymbol and
mi.addVar) causes duplicate wrapper bindings; remove this post-collection pass
or change it to skip adding if the binding already exists (e.g., check
ModuleInfo via mi.hasVar/ equivalent before calling mi.addVar) so wrapper refs
produced by final out_stmts are not added a second time.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/bundler/linker_context/postProcessJSChunk.zig`:
- Around line 202-218: The loop that re-adds wrapper refs (for
chunk.content.javascript.parts_in_chunk_in_order using
all_flags/all_wrapper_refs and calling chunk.renamer.nameForSymbol and
mi.addVar) causes duplicate wrapper bindings; remove this post-collection pass
or change it to skip adding if the binding already exists (e.g., check
ModuleInfo via mi.hasVar/ equivalent before calling mi.addVar) so wrapper refs
produced by final out_stmts are not added a second time.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 08bff5e5-fe9e-4fda-8750-3ae94b3847d1

📥 Commits

Reviewing files that changed from the base of the PR and between 5740da8 and 7cf2d8e.

📒 Files selected for processing (5)
  • src/bundler/bundle_v2.zig
  • src/bundler/linker_context/generateCodeForFileInChunkJS.zig
  • src/bundler/linker_context/generateCompileResultForJSChunk.zig
  • src/bundler/linker_context/postProcessJSChunk.zig
  • test/bundler/bundler_compile.test.ts

PR #27838 changed tls_props from ?SSLConfig to ?SSLConfig.SharedPtr but
missed updating getTlsHostname(), causing a build failure on main.

https://claude.ai/code/session_01VUffTTZPCvYZwcopKhoiAf

@claude claude 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.

This PR refactors how the bundler collects external imports for bytecode ModuleInfo, moving from original AST scanning to converted-statement collection. The logic changes in postProcessJSChunk and DeclCollector are non-trivial and touch a critical code generation path, so a human reviewer should verify the approach.

Extended reasoning...

Overview

This PR fixes a bug where bun build --compile --bytecode --minify --format esm fails with "Cannot find module" errors for named imports from npm packages with re-export index files. It touches 4 Zig source files in the bundler linker context (bundle_v2.zig, generateCodeForFileInChunkJS.zig, generateCompileResultForJSChunk.zig, postProcessJSChunk.zig) and adds a test case. The core change is extending DeclCollector to also capture s_import statements from converted output, then using those collected imports in postProcessJSChunk instead of scanning the original AST.

Security risks

No direct security risks. The changes are internal to the bundler’s code generation pipeline and do not affect auth, permissions, or user input handling.

Level of scrutiny

High scrutiny warranted. This modifies the bundler’s linker context — specifically how external imports are collected for bytecode ModuleInfo. Incorrect collection could cause runtime failures (modules not found) or incorrect module metadata. The PR carries a claude label indicating AI authorship, which adds reason for careful human review of the Zig logic.

Other factors

  • There is an unaddressed CodeRabbit suggestion about adding bounds checks on import_info.source_index before indexing into all_import_records and parts arrays. While the source_index originates from part_range.source_index.get() (which should be valid), the defensive check is reasonable for a safety-critical path.
  • The test coverage looks good — a new PackageReExportNamedImport scenario is added that exercises the exact bug fix, and it runs in both default and minified configurations via the existing test matrix.
  • CI was recently rebased onto main; results pending on commit 7cf2d8e.

@claude claude 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.

This PR changes how the bundler linker collects external imports for bytecode ModuleInfo, threading a new ImportInfoCollected struct through the parallel printing pipeline. The fix looks reasonable, but the architectural change to bundler internals warrants a human look before merging.

Extended reasoning...

Overview

This PR fixes #27454 where bun build --compile --bytecode --minify --format esm fails with "Cannot find module" errors for packages with re-exported named imports. It modifies 4 bundler linker files (bundle_v2.zig, generateCodeForFileInChunkJS.zig, generateCompileResultForJSChunk.zig, postProcessJSChunk.zig), plus an unrelated http.zig rebase fix and a new test case.

The core change: instead of scanning the original AST for external imports (where record.source_index.isValid() may not be patched for transitive dependencies resolved through the async pipeline), DeclCollector now captures s_import statements from converted output, and postProcessJSChunk iterates these collected imports. It still looks up the original AST to get binding info (default_name, items, namespace_ref).

Security risks

No security concerns — this is bundler-internal metadata collection for bytecode generation. No user input handling, no auth, no network-facing changes.

Level of scrutiny

This needs human review. The change is architecturally significant: it introduces a new data structure (ImportInfoCollected) threaded through the parallel printing pipeline (DeclCollectorCompileResultpostProcessJSChunk). The bundler linker context is a critical and complex code path. Additionally, this is an AI-generated PR (labeled claude) with no human code review yet — a human should verify the approach is the right architectural choice and that the interaction between collected imports and original AST lookups is correct in all edge cases.

Other factors

  • The pre-existing bug (AST search missing s_export_from/s_export_star converted to s_import) is noted but not introduced by this PR
  • CodeRabbit's bounds-check suggestion was correctly dismissed by robobun
  • The regression test (PackageReExportNamedImport) covers the specific scenario
  • CI failures appear unrelated (tinycc warnings, CMakeLists warnings)

Comment on lines +156 to +168
const s_import = brk: {
for (source_parts) |part| {
for (part.stmts) |stmt| {
switch (stmt.data) {
.s_import => |s| {
if (s.import_record_index == import_info.import_record_index)
break :brk s;
},
else => {},
}
}
}
continue;

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.

🟣 Pre-existing bug: The AST search at lines 156-168 only matches .s_import when looking up the original statement for a collected import, but convertStmtsForChunk converts s_export_from and s_export_star (with alias) into S.Import — the original AST still has the unconverted statement types, so the search misses them and silently drops the import from ModuleInfo via continue at line 168. This affects --compile --bytecode --format esm builds with re-exports of external modules (e.g., export { readFileSync } from "node:fs"). This issue exists in the old code as well and is not introduced by this PR.

Extended reasoning...

What the bug is

In postProcessJSChunk.zig lines 156-168, the code iterates over collected imports from DeclCollector and then searches the original AST parts for a matching s_import statement by import_record_index. The search only matches the .s_import variant (line 160). If no match is found, it falls through to continue at line 168, silently skipping the import.

Why it fails for re-exports

convertStmtsForChunk transforms s_export_from into S.Import (lines 308-329 of convertStmtsForChunk.zig) and s_export_star with alias into S.Import (lines 108-118) when shouldStripExports is true (always true in bundle mode). DeclCollector correctly captures these converted s_import statements with their import_record_index. However, the original AST — which is what postProcessJSChunk searches — still has .s_export_from or .s_export_star, not .s_import. The search at line 160 never matches these statement types.

Step-by-step proof

  1. A source file has export { readFileSync } from "node:fs" in a --compile --bytecode --format esm build.
  2. The parser creates an s_export_from statement with an import_record_index pointing to node:fs.
  3. convertStmtsForChunk converts this to S.Import (line 320-329) because shouldStripExports is true and shouldRemoveImportExportStmt returns false (external ESM import).
  4. DeclCollector.collectFromStmts sees the converted .s_import and records { import_record_index, source_index } into dc.imports.
  5. postProcessJSChunk iterates imports and finds this entry. It looks up the import record — valid, not disabled, not bun-tagged.
  6. It searches the original AST parts for .s_import with matching import_record_index (line 160). The original AST has .s_export_from, not .s_import.
  7. The search exhausts all parts and falls through to continue at line 168. The import is silently dropped from ModuleInfo.

Impact

In --compile --bytecode --format esm builds, re-exports of external modules (export { ... } from "ext" or export * as ns from "ext") will have incomplete ModuleInfo — their dependency tracking metadata is missing. The bytecode itself still works correctly; only the dependency metadata is affected. This is an edge case requiring external re-export syntax in a bytecode ESM compile build.

Pre-existing nature

The old code (before this PR) also only matched .s_import when scanning the original AST — s_export_from fell into the else branch. Both old and new code have the same limitation. The PR does not introduce or worsen this bug.

Fix

The search block (lines 159-164) should also match .s_export_from and .s_export_star by import_record_index, extracting the relevant fields (items, namespace_ref) from those statement types to reconstruct the import bindings for ModuleInfo.

@robobun robobun closed this May 22, 2026
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.

1.3.10 - build output with bytecode+minify+esm fails to run

3 participants