bundler: propagate namespace requests through chained barrels - #36838
Conversation
A namespace import (import * as ns) of a barrel only un-deferred the barrel's own import records. Names the barrel re-exports from an inner barrel that was already parsed with a partial request set were never requested there, so the inner barrel's records stayed deferred: the module bodies were silently dropped from the output while the namespace object still referenced their symbols, producing chunks that fail with "Exported binding 'X' needs to refer to a top-level declared variable" even though the build reported success. Which case you got depended on parse-completion order, which also made chunk assignment flap between runs of identical inputs. Fix, in scheduleBarrelDeferredImports: - The BFS star branch now resolves un-deferred records inline and then propagates the request onward: each re-exported name is requested from the module it comes from (original alias), namespace re-exports and 'export *' targets are requested as full-namespace. - Star items mark a barrel as fully requested at most once, keeping the new propagation terminating on 'export *' cycles. - 'export * from' targets are recorded as fully requested when the star exporter is processed, mirroring what the IS_EXPORT_STAR_TARGET check in applyBarrelOptimization does when the exporter happens to parse first, so the deferral decision no longer depends on parse order. On the reporter's reproduction (200-build loops that previously produced three distinct outputs, one of which dropped 31 @sentry/core modules), the output is now byte-identical across runs and every chunk links. Fixes #36832
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughChangesBarrel namespace propagation
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/bundler/barrel_imports.rs`:
- Around line 615-645: Update the export-star propagation loop around
RequestedExports::entry so it only assigns RequestedExports::All and enqueues a
BarrelWorkItem when the target’s existing request is not already All. Reuse the
same deduplication condition used near line 703, preserving propagation for
newly or partially requested targets while skipping redundant full requests.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 22a6069f-b28d-40e6-9bbd-bff12660dd8c
📒 Files selected for processing (2)
src/bundler/barrel_imports.rstest/bundler/bundler_barrel.test.ts
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/bundler/barrel_imports.rs (1)
785-786: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winResolve nested barrel targets with the DevServer path fallback.
DevServer records can have an invalid
rec.source_index; Lines 452-476 already handle this withpath_to_source_index_map. These new guards skip both named re-exports andexport *targets instead of resolving their paths. A chained barrel can therefore lose namespace propagation and omit modules.Reuse the existing source-index/path fallback, including the HMR dedup fallback, before skipping the record. Add a DevServer regression test for a chained namespace re-export.
Also applies to: 812-813
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/bundler/barrel_imports.rs` around lines 785 - 786, Update the source-index handling for both named re-exports and export-* targets near the invalid rec.source_index guards, reusing the existing path_to_source_index_map resolution and HMR dedup fallback from lines 452-476 before skipping a record. Preserve skipping only when no valid source index can be recovered, and add a DevServer regression test covering chained namespace re-exports.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/bundler/barrel_imports.rs`:
- Around line 785-786: Update the source-index handling for both named
re-exports and export-* targets near the invalid rec.source_index guards,
reusing the existing path_to_source_index_map resolution and HMR dedup fallback
from lines 452-476 before skipping a record. Preserve skipping only when no
valid source index can be recovered, and add a DevServer regression test
covering chained namespace re-exports.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5d53401e-83d1-4edf-904d-c0ffb97e246e
📒 Files selected for processing (1)
src/bundler/barrel_imports.rs
|
Addressed the outside-diff review note in 1b688b8: the star-propagation loops now fall back to the path map when source_index is unset (dev server mode), matching the seeding loops earlier in the function. Barrel suite and test/bake/dev/bundle.test.ts pass. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/bundler/barrel_imports.rs`:
- Around line 787-796: Extract the duplicated source-index resolution logic into
a named helper near the surrounding bundler utilities, accepting the record’s
source index, path, and optional path-to-source-index map and returning the
resolved target as an Option. Replace the inline fallback blocks in the affected
sites within the function, including both new blocks around the target
resolution, so each call site consistently skips when the helper returns None
while preserving the existing valid-index and map-lookup behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: c282bba4-7d05-4ed0-b004-ad9180c89481
📒 Files selected for processing (1)
src/bundler/barrel_imports.rs
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/bundler/barrel_imports.rs (1)
743-743: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winUse
usizeinun_defer_record.import_record::Listusesusizelengths, and nou32bound is declared. Passingidxdirectly removes this panic path.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/bundler/barrel_imports.rs` at line 743, Update the call to un_defer_record in the barrel import processing flow to pass idx directly as a usize, removing the u32::try_from conversion and its unwrap panic path. Ensure un_defer_record accepts usize consistently with import_record::List lengths.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/bundler/barrel_imports.rs`:
- Line 743: Update the call to un_defer_record in the barrel import processing
flow to pass idx directly as a usize, removing the u32::try_from conversion and
its unwrap panic path. Ensure un_defer_record accepts usize consistently with
import_record::List lengths.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: cace2df2-41fe-43c2-becf-29c710546f61
📒 Files selected for processing (1)
src/bundler/barrel_imports.rs
|
CI status: the only red on the latest run (build 88199) is a segfault in test/js/bun/util/filesystem_router.test.ts on the Alpine aarch64 lane, which this diff does not touch (reported to main-break triage separately); the napi failure passed on retry. The bundler changes and both regression tests are green on all lanes. Ready for review. |
There was a problem hiding this comment.
Both earlier notes are addressed — the BackRef safety comment now states the interleaved-reborrow invariant, and barrel/NamespaceImportRequestsExportStarTargets pins the export-* seeding path. No new findings, but this reworks the barrel BFS propagation with interleaved &mut/BackRef borrows in a path whose failure mode is silent output corruption, so it's worth a maintainer look.
Also checked: the already-All skip terminates both export * cycles and namespace-re-export cycles (every push of a star item goes through a path that sets All first); record_target matches the inline pattern it replaced at all five &path-shaped sites; the star-branch propagation uses imp.alias (the original name in the source module), consistent with how the non-star branch propagates via resolve_barrel_export.
Extended reasoning...
Overview
Two files: src/bundler/barrel_imports.rs (+138/-24) and test/bundler/bundler_barrel.test.ts (+112). The Rust change extends schedule_barrel_deferred_imports so that (1) a namespace/star request against an already-parsed barrel propagates onward to every module that barrel re-exports from — named re-exports by their original alias, export * and namespace re-exports as full-namespace — and (2) export * from targets are recorded as fully requested when the exporter is processed, independent of parse order. A new record_target helper deduplicates the source-index/path-map fallback across five call sites, un_defer_record now takes usize, and the BackRef borrowck comment was updated to reflect that resolve_barrel_records may reborrow the map &mut between derefs.
Security risks
None. This is bundler graph-scheduling logic; no untrusted input parsing, no I/O, no auth/crypto.
Level of scrutiny
High. The failure mode of a bug here is the one this PR fixes: success: true with a broken bundle. The change sits inside a BFS with manual borrow-lifetime reasoning (BackRef derefs interleaved with &mut reborrows via resolve_barrel_records), a local StarPush staging vec to satisfy borrowck, and cycle-termination that depends on every star-push path having already set RequestedExports::All. That is not mechanical; someone who owns this subsystem should confirm the propagation shape matches the linker's expectations and that the extra resolve_barrel_records calls inside the star branch don't interact badly with the dev-server path.
Other factors
All prior review threads are resolved: comment-cop's long-comment flags were shortened, CodeRabbit's dedup and helper-extraction suggestions were applied, and my two earlier inline notes (stale BackRef invariant comment; missing export * test coverage) were addressed in 9e9bec9 and 2d34dde respectively. The two new tests use a 5000-line filler file to bias parse order deterministically, assert both output content and runtime stdout, and were verified fail-before/pass-after against current main. The existing barrel suite passes on both debug+ASAN and release per the PR evidence.
…h#36838) ### What does this PR do? Fixes oven-sh#36832. `Bun.build({ splitting: true })` on a tree with chained `sideEffects: false` barrel packages (e.g. `@sentry/node-core` re-exporting `@sentry/core`) produced non-deterministic output, and in the unlucky parse order silently dropped modules: the build reported `success: true` while the emitted chunks referenced symbols no chunk declares, failing at boot with ``` SyntaxError: Exported binding 'tK' needs to refer to a top-level declared variable. ``` ### Cause Barrel optimization defers a barrel's unused re-export records at parse time and un-defers them later as requests for the names arrive (`scheduleBarrelDeferredImports`). Two of those request paths depended on parse-completion order: 1. A namespace request (`import * as ns from 'pkg-a'`) arriving after the barrel was parsed only un-deferred the barrel's own records. The names the barrel re-exports from an inner barrel (`export { beta } from 'pkg-b'`) were never requested from `pkg-b` when `pkg-b` had already been parsed with a partial request set. `pkg-b`'s records stayed deferred, the module bodies were dropped from the output, and the namespace object still referenced their symbols. This is the dropped-module case in the issue: the same 31 `@sentry/core` modules missing, 18 orphaned minified bindings. 2. `export * from` targets are meant to be exempt from deferral (the `IS_EXPORT_STAR_TARGET` check in `applyBarrelOptimization`), but the flag only lands if the star exporter parses before the target. This is what flapped the chunk graph (25 vs 32 chunks for identical input). ### Fix In `src/bundler/barrel_imports.rs`: - The BFS star branch now resolves un-deferred records inline and propagates the request onward: each re-exported name is requested from the module it comes from (by its original alias); namespace re-exports and `export *` targets are requested as full-namespace. - Star items mark a barrel as fully requested at most once, so the new propagation terminates on `export *` cycles. - `export * from` targets are recorded as fully requested when the star exporter is processed, making the deferral decision independent of parse order (same outcome the `IS_EXPORT_STAR_TARGET` flag produces when the exporter happens to parse first). ### Verification On the reporter's repro (https://github.com/Karavil/bun-splitting-orphaned-exports), the unfixed build produced three distinct outputs across runs (25-chunk good, 32-chunk variant, 25-chunk bad with 12 unlinkable chunks, ~2-4% of runs on a 16-core box). With the fix, output is byte-identical across every run, all chunks link when re-bundled individually, and the bundle boots. New test `barrel/NamespaceImportUndefersChainedBarrels` reconstructs the bad parse order deterministically: the file holding `import * as ns` is large enough that the small barrel files always parse (and defer) first. It fails on the unfixed build every time (module body missing from output, boot fails) and passes with the fix. Existing barrel, splitting, and edgecase bundler suites pass. <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 1 · 2 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: BUILD FAILED (no junit output) $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/bundler_barrel.test.ts ninja: Entering directory `/workspace/bun/build/debug' [1/8] gen cpp.rs (cppbind) [2/8] gen generated_host_exports.rs generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 238 extern-C blocks audited [2/8] 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) [3/8] cxx obj/unified/UnifiedSource-src_jsc_bindings-0.cpp.o FAILED: obj/unified/UnifiedSource-src_jsc_bindings-0.cpp.o /usr/bin/ccache /usr/lib/llvm-21/bin/clang++ -march=nehalem -O0 -g3 -gz=zstd -glldb -fsanitize=address -fno-exceptions -fno-c++-static-destructors -fno-rtti -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fvisibility=hidden -fvisibility-inlines-hidden -fno-unwind-tables -fno-asynchronous-unwind-tables -Wno-c23-extensions -ffunction-sections -fdata-sections -faddrsig -fno-semantic-interposition -fno-delete-null-pointer-checks -fdiagnostics-color=always -ferror-limit=100 -std=gnu++23 ... (truncated) release without fix: all passed bun test v1.4.0-canary.1 (5c9b728) test/bundler/bundler_barrel.test.ts: (pass) bundler > barrel/SkipUnusedWithOptimizeImports [13.95ms] (pass) bundler > barrel/AllExportsNeeded [3.87ms] (pass) bundler > barrel/SkipUnusedWithSideEffectsFalse [4.67ms] (pass) bundler > barrel/NoOptimizationWithoutSideEffects [3.30ms] (pass) bundler > barrel/ExportStarLoadsAll [2.62ms] (pass) bundler > barrel/NonBarrelWithLocalExports [2.60ms] (pass) bundler > barrel/NamespaceImportLoadsAll [2.71ms] (pass) bundler > barrel/OutputEquivalence [3.66ms] (pass) bundler > barrel/DefaultReExport [3.58ms] (pass) bundler > barrel/ImportThenExport [3.44ms] (pass) bundler > barrel/ReExportChain [3.52ms] (pass) bundler > barrel/StarWithNamedFromSameSource [2.46ms] (pass) bundler > barrel/SideEffectOnlyImport [3.32ms] (pass) bundler > barrel/MultipleImporters [3.47ms] (pass) bundler > barrel/CircularExports [11.45ms] (pass) bundler > barrel/CircularStarExports [12.35ms] (pass) bundler > barrel/NamespaceReExportCycleThroughStarTarget [13.21ms] (pass) bundler > barrel/SelfReExport [6.58ms] (pass) bundler > barrel/DynamicImportInSubmodule [4.67ms] (pass) bundler > barrel/DynamicImportWithStaticImpor ... (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/bundler/bundler_barrel.test.ts bun test v1.4.0 (2d34dde) test/bundler/bundler_barrel.test.ts: (pass) bundler > barrel/SkipUnusedWithOptimizeImports [602.47ms] (pass) bundler > barrel/AllExportsNeeded [118.09ms] (pass) bundler > barrel/SkipUnusedWithSideEffectsFalse [91.17ms] (pass) bundler > barrel/NoOptimizationWithoutSideEffects [99.23ms] (pass) bundler > barrel/ExportStarLoadsAll [84.00ms] (pass) bundler > barrel/NonBarrelWithLocalExports [89.63ms] (pass) bundler > barrel/NamespaceImportLoadsAll [87.82ms] (pass) bundler > barrel/OutputEquivalence [109.01ms] (pass) bundler > barrel/DefaultReExport [116.58ms] (pass) bundler > barrel/ImportThenExport [111.44ms] (pass) bundler > barrel/ReExportChain [94.02ms] (pass) bundler > barrel/StarWithNamedFromSameSource [86.77ms] (pass) bundler > barrel/SideEffectOnlyImport [111.60ms] (pass) bundler > barrel/MultipleImporters [99.50ms] (pass) bundler > barrel/CircularExports [372.92ms] (pass) bundler > barrel/CircularStarExports [401.76ms] (pass) bundler > barrel/NamespaceReExportCycleThr ... (truncated) release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 681ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/9] gen cpp.rs (cppbind) [2/9] gen generated_host_exports.rs generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 238 extern-C blocks audited [2/9] 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/boringssl_sys) �[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety) �[1m�[92m Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys) �[1m�[92m Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys) �[1m�[92m Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd) �[1m�[92m Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp) �[1m�[92m ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/bundler/barrel_imports.rs | 162 ++++++++++++++++++++++++++++++------ test/bundler/bundler_barrel.test.ts | 112 +++++++++++++++++++++++++ 2 files changed, 250 insertions(+), 24 deletions(-) ``` </details> **gate history** · 4 passed · 0 rejected · iteration 1 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/bundler/barrel_imports.rs 10 34 0 test/bundler/bundler_barrel.test.ts 2 4 0 ``` </details> **root cause** · written by the author bot The bundler's barrel import deferral failed to propagate namespace requests through chained barrel re-exports, and `export * from` targets were only marked fully requested when the exporter parsed before the target, making module inclusion dependent on the non-deterministic parallel parse order. When an unfavorable order occurred, deferred modules were never scheduled and were silently dropped from the output while other chunks still referenced their minified exports. The fix propagates full-namespace and aliased requests through both named and star re-exports during the BFS and seeds expor… <!-- robobun:evidence:end --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
### Problem
- A request for every export of a file (`import * as`, `export * from`,
`import()`, `require()`) makes the bundler load the target of an import
the parser dropped: an unused TypeScript import or a macro import.
`import * as ns from "./a"` fails with `Could not resolve:
"missing-pkg"` from a file that only a type import in `a.ts` names. A
macro module that imports `bun` fails `--target=browser`: `Browser build
cannot import Bun builtin: "bun"`.
- The cause is `un_defer_record` (`src/bundler/barrel_imports.rs:296`).
It clears `IS_UNUSED`, which barrel optimization uses to defer a
re-export. The parser sets the same flag on imports it drops, and the
request walks every record of the target file.
- A failed build reported one or two errors for the same 6 files, by
parse order.
### Fix
- `apply_barrel_optimization` marks each record it defers with the new
`ImportRecordFlags::IS_BARREL_DEFERRED`. `un_defer_record` acts only on
that flag.
- A file that fails to resolve drops the flag from its records: its
imports are not followed.
- Correct because the parser removes the statement of every other
`IS_UNUSED` record. esbuild agrees on each repro.
- Verified: `test/bundler/bundler_barrel.test.ts`, 8 new tests, each
fails on main. Other suites in Notes.
### Background
- A barrel is a `sideEffects: false` module whose exports are all
re-exports. `apply_barrel_optimization` defers the re-exports nobody
asked for: the record gets `IS_UNUSED`, so its target does not load.
- `schedule_barrel_deferred_imports` runs for every file and un-defers
what it requests from each target. A namespace request needs every
export.
- A file whose resolution fails stays in the graph as an empty row plus
its import records.
<details><summary>Notes</summary>
Deterministic repro (1.4.2 and main fail, 1.3.9 and this branch build):
```sh
printf 'import { T } from "./types";\nexport const value: T = 1;\n' > a.ts
printf 'import "missing-pkg";\nexport type T = number;\n' > types.ts
printf 'import * as ns from "./a";\nconsole.log(ns.value);\n' > entry.ts
bun build --target=bun ./entry.ts
# error: Could not resolve: "missing-pkg" at types.ts:1:8
# `import { value } from "./a"` builds on every version
```
Macro form:
```sh
printf 'import { version } from "bun";\nexport function getValue() { return typeof version; }\n' > m.ts
printf 'import { getValue } from "./m.ts" with { type: "macro" };\nexport const v = getValue();\n' > a.ts
printf 'import * as ns from "./a.ts";\nconsole.log(ns.v);\n' > star.ts
bun build --target=browser ./star.ts
# error: Browser build cannot import Bun builtin: "bun" at m.ts:1:25
```
The fuzz report that led here (same 6 files, 300 builds in one process,
release build of 5fce36e):
```
e0.ts: await import("./f0.ts") f0.ts: export * from "./f2.ts"
e1.ts: await import("./f2.ts") f2.ts: import { get5, C5 } from "./f5.ts"; import { fn } from "missing-pkg"; export const two = fn
e2.ts: await import("./f7.ts") f5.ts: import "./f7.ts"
f7.ts: import { fn } from "missing-pkg"; export const seven = fn
entries e0 e1: 174x [f2.ts:2] 126x [f2.ts:2, f7.ts:1]
entries e0 e1 e2: 207x [f2.ts:2, f7.ts:1] 93x [f2.ts:2, f7.ts:1, f7.ts:1]
this branch: 60/60 [f2.ts:2] 60/60 [f2.ts:2, f7.ts:1]
```
Why it looked like a race. `f2.ts` fails to resolve `missing-pkg`, so
`on_parse_task_complete` takes the `Err` arm and never calls
`schedule_barrel_deferred_imports` for it.
`run_resolution_for_parse_task` keeps the import records of a failed
file on the graph (for pending plugin `onResolve` answers). When `f0.ts`
(`export * from "./f2.ts"`) finishes after that, its request walks those
records, clears `IS_UNUSED` on the dropped `./f5.ts` import, resolves
it, and `f5.ts` and `f7.ts` load. When `f0.ts` finishes first, the row
of `f2.ts` is still empty and nothing happens. With `f2.ts` free of its
own error the walk always runs (from `f0.ts` or from the pass of `f2.ts`
itself), so the `f7.ts` error appears in every build. That error is not
a real error of the build: `f5.ts` and `f7.ts` sit behind an import
TypeScript drops, and esbuild builds that tree.
Why the same error twice. `resolve_barrel_records` reads
`graph.ast.items_target()[idx]`. The row of a failed file is
`JSAst::empty`, whose target is `browser`. The build target was `bun`,
so `./f5.ts` resolved into the browser graph, and `f7.ts`, already
parsed once through `e2.ts`, parsed again there.
History. Barrel optimization landed in 1.3.10 (#26892) and the `import *
as` form fails from that release on. #36838 (1.4.0) made `export * from`
a full request as well. The report guessed #41145 for the rise in
frequency at 1.4.1. That PR changes the linker, which a failed build
never reaches. I did not bisect what moved the rate.
Why a flag and not a narrower walk. The walk could visit only the
records that named exports refer to, which is the set
`apply_barrel_optimization` can defer. Under HMR,
`ConvertESMExportsForHmr` also marks the second of two `export { .. }
from "./same.js"` records `IS_UNUSED`, and the named-export path of the
BFS un-deferred it too. The flag is exact for all three call sites of
`un_defer_record`. A record that is both an HMR duplicate and deferred
still un-defers as before. `ImportRecordFlags` is a `u32` with bits 0 to
18 in use.
Not fixed here. A barrel that first parses with every record deferred,
and whose un-deferred record later fails to resolve, is not a failed
file: `resolve_barrel_records` ignores `last_error`, and its other
records can still un-defer. So a build that fails inside a barrel can
still report a different set of additional errors from run to run.
Making that deterministic means following the resolvable imports of a
failed file, which is a larger change.
Suites run on the debug build: `bundler_barrel` (70 pass),
`bundler_splitting`, `bundler_dynamic_import_dce`, `bundler_cjs2esm`,
`bundler_edgecase`, `bundler_plugin`, `esbuild/ts`,
`esbuild/importstar`, `esbuild/importstar_ts`, `esbuild/dce`,
`esbuild/default`, `regression/issue/40606`, `30493`, `28170`, and the
barrel tests of `bake/dev/bundle.test.ts`. `bun-build-api` has two 5 s
timeouts in bytecode tests under the debug build, the same on main. With
`src/` at main, all 8 new tests fail on the debug build (6 of 6 runs for
the three that bias parse order with a 4 MB comment) and on the release
build.
</details>
…-sh#42737) ### Problem - A request for every export of a file (`import * as`, `export * from`, `import()`, `require()`) makes the bundler load the target of an import the parser dropped: an unused TypeScript import or a macro import. `import * as ns from "./a"` fails with `Could not resolve: "missing-pkg"` from a file that only a type import in `a.ts` names. A macro module that imports `bun` fails `--target=browser`: `Browser build cannot import Bun builtin: "bun"`. - The cause is `un_defer_record` (`src/bundler/barrel_imports.rs:296`). It clears `IS_UNUSED`, which barrel optimization uses to defer a re-export. The parser sets the same flag on imports it drops, and the request walks every record of the target file. - A failed build reported one or two errors for the same 6 files, by parse order. ### Fix - `apply_barrel_optimization` marks each record it defers with the new `ImportRecordFlags::IS_BARREL_DEFERRED`. `un_defer_record` acts only on that flag. - A file that fails to resolve drops the flag from its records: its imports are not followed. - Correct because the parser removes the statement of every other `IS_UNUSED` record. esbuild agrees on each repro. - Verified: `test/bundler/bundler_barrel.test.ts`, 8 new tests, each fails on main. Other suites in Notes. ### Background - A barrel is a `sideEffects: false` module whose exports are all re-exports. `apply_barrel_optimization` defers the re-exports nobody asked for: the record gets `IS_UNUSED`, so its target does not load. - `schedule_barrel_deferred_imports` runs for every file and un-defers what it requests from each target. A namespace request needs every export. - A file whose resolution fails stays in the graph as an empty row plus its import records. <details><summary>Notes</summary> Deterministic repro (1.4.2 and main fail, 1.3.9 and this branch build): ```sh printf 'import { T } from "./types";\nexport const value: T = 1;\n' > a.ts printf 'import "missing-pkg";\nexport type T = number;\n' > types.ts printf 'import * as ns from "./a";\nconsole.log(ns.value);\n' > entry.ts bun build --target=bun ./entry.ts # error: Could not resolve: "missing-pkg" at types.ts:1:8 # `import { value } from "./a"` builds on every version ``` Macro form: ```sh printf 'import { version } from "bun";\nexport function getValue() { return typeof version; }\n' > m.ts printf 'import { getValue } from "./m.ts" with { type: "macro" };\nexport const v = getValue();\n' > a.ts printf 'import * as ns from "./a.ts";\nconsole.log(ns.v);\n' > star.ts bun build --target=browser ./star.ts # error: Browser build cannot import Bun builtin: "bun" at m.ts:1:25 ``` The fuzz report that led here (same 6 files, 300 builds in one process, release build of 5fce36e): ``` e0.ts: await import("./f0.ts") f0.ts: export * from "./f2.ts" e1.ts: await import("./f2.ts") f2.ts: import { get5, C5 } from "./f5.ts"; import { fn } from "missing-pkg"; export const two = fn e2.ts: await import("./f7.ts") f5.ts: import "./f7.ts" f7.ts: import { fn } from "missing-pkg"; export const seven = fn entries e0 e1: 174x [f2.ts:2] 126x [f2.ts:2, f7.ts:1] entries e0 e1 e2: 207x [f2.ts:2, f7.ts:1] 93x [f2.ts:2, f7.ts:1, f7.ts:1] this branch: 60/60 [f2.ts:2] 60/60 [f2.ts:2, f7.ts:1] ``` Why it looked like a race. `f2.ts` fails to resolve `missing-pkg`, so `on_parse_task_complete` takes the `Err` arm and never calls `schedule_barrel_deferred_imports` for it. `run_resolution_for_parse_task` keeps the import records of a failed file on the graph (for pending plugin `onResolve` answers). When `f0.ts` (`export * from "./f2.ts"`) finishes after that, its request walks those records, clears `IS_UNUSED` on the dropped `./f5.ts` import, resolves it, and `f5.ts` and `f7.ts` load. When `f0.ts` finishes first, the row of `f2.ts` is still empty and nothing happens. With `f2.ts` free of its own error the walk always runs (from `f0.ts` or from the pass of `f2.ts` itself), so the `f7.ts` error appears in every build. That error is not a real error of the build: `f5.ts` and `f7.ts` sit behind an import TypeScript drops, and esbuild builds that tree. Why the same error twice. `resolve_barrel_records` reads `graph.ast.items_target()[idx]`. The row of a failed file is `JSAst::empty`, whose target is `browser`. The build target was `bun`, so `./f5.ts` resolved into the browser graph, and `f7.ts`, already parsed once through `e2.ts`, parsed again there. History. Barrel optimization landed in 1.3.10 (oven-sh#26892) and the `import * as` form fails from that release on. oven-sh#36838 (1.4.0) made `export * from` a full request as well. The report guessed oven-sh#41145 for the rise in frequency at 1.4.1. That PR changes the linker, which a failed build never reaches. I did not bisect what moved the rate. Why a flag and not a narrower walk. The walk could visit only the records that named exports refer to, which is the set `apply_barrel_optimization` can defer. Under HMR, `ConvertESMExportsForHmr` also marks the second of two `export { .. } from "./same.js"` records `IS_UNUSED`, and the named-export path of the BFS un-deferred it too. The flag is exact for all three call sites of `un_defer_record`. A record that is both an HMR duplicate and deferred still un-defers as before. `ImportRecordFlags` is a `u32` with bits 0 to 18 in use. Not fixed here. A barrel that first parses with every record deferred, and whose un-deferred record later fails to resolve, is not a failed file: `resolve_barrel_records` ignores `last_error`, and its other records can still un-defer. So a build that fails inside a barrel can still report a different set of additional errors from run to run. Making that deterministic means following the resolvable imports of a failed file, which is a larger change. Suites run on the debug build: `bundler_barrel` (70 pass), `bundler_splitting`, `bundler_dynamic_import_dce`, `bundler_cjs2esm`, `bundler_edgecase`, `bundler_plugin`, `esbuild/ts`, `esbuild/importstar`, `esbuild/importstar_ts`, `esbuild/dce`, `esbuild/default`, `regression/issue/40606`, `30493`, `28170`, and the barrel tests of `bake/dev/bundle.test.ts`. `bun-build-api` has two 5 s timeouts in bytecode tests under the debug build, the same on main. With `src/` at main, all 8 new tests fail on the debug build (6 of 6 runs for the three that bias parse order with a 4 MB comment) and on the release build. </details>
What does this PR do?
Fixes #36832.
Bun.build({ splitting: true })on a tree with chainedsideEffects: falsebarrel packages (e.g.@sentry/node-corere-exporting@sentry/core) produced non-deterministic output, and in the unlucky parse order silently dropped modules: the build reportedsuccess: truewhile the emitted chunks referenced symbols no chunk declares, failing at boot withCause
Barrel optimization defers a barrel's unused re-export records at parse time and un-defers them later as requests for the names arrive (
scheduleBarrelDeferredImports). Two of those request paths depended on parse-completion order:import * as ns from 'pkg-a') arriving after the barrel was parsed only un-deferred the barrel's own records. The names the barrel re-exports from an inner barrel (export { beta } from 'pkg-b') were never requested frompkg-bwhenpkg-bhad already been parsed with a partial request set.pkg-b's records stayed deferred, the module bodies were dropped from the output, and the namespace object still referenced their symbols. This is the dropped-module case in the issue: the same 31@sentry/coremodules missing, 18 orphaned minified bindings.export * fromtargets are meant to be exempt from deferral (theIS_EXPORT_STAR_TARGETcheck inapplyBarrelOptimization), but the flag only lands if the star exporter parses before the target. This is what flapped the chunk graph (25 vs 32 chunks for identical input).Fix
In
src/bundler/barrel_imports.rs:export *targets are requested as full-namespace.export *cycles.export * fromtargets are recorded as fully requested when the star exporter is processed, making the deferral decision independent of parse order (same outcome theIS_EXPORT_STAR_TARGETflag produces when the exporter happens to parse first).Verification
On the reporter's repro (https://github.com/Karavil/bun-splitting-orphaned-exports), the unfixed build produced three distinct outputs across runs (25-chunk good, 32-chunk variant, 25-chunk bad with 12 unlinkable chunks, ~2-4% of runs on a 16-core box). With the fix, output is byte-identical across every run, all chunks link when re-bundled individually, and the bundle boots.
New test
barrel/NamespaceImportUndefersChainedBarrelsreconstructs the bad parse order deterministically: the file holdingimport * as nsis large enough that the small barrel files always parse (and defer) first. It fails on the unfixed build every time (module body missing from output, boot fails) and passes with the fix. Existing barrel, splitting, and edgecase bundler suites pass.[review] gate passed · iteration 1 · 2 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 4 passed · 0 rejected · iteration 1
evidence per changed file
root cause · written by the author bot
The bundler's barrel import deferral failed to propagate namespace requests through chained barrel re-exports, and
export * fromtargets were only marked fully requested when the exporter parsed before the target, making module inclusion dependent on the non-deterministic parallel parse order. When an unfavorable order occurred, deferred modules were never scheduled and were silently dropped from the output while other chunks still referenced their minified exports. The fix propagates full-namespace and aliased requests through both named and star re-exports during the BFS and seeds expor…