Skip to content

bundler: propagate namespace requests through chained barrels - #36838

Merged
Jarred-Sumner merged 10 commits into
mainfrom
farm/5b28b337/fix-barrel-splitting-nondeterminism
Aug 3, 2026
Merged

Jarred-Sumner merged 10 commits into
mainfrom
farm/5b28b337/fix-barrel-splitting-nondeterminism

Conversation

@robobun

@robobun robobun commented Aug 3, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes #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.


[review] gate passed · iteration 1 · 2 files touched

fails on main (without fix)
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 (5c9b72894)

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)
passes on PR (with fix)
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 (2d34ddef8)

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)
diff hotspot
src/bundler/barrel_imports.rs       | 162 ++++++++++++++++++++++++++++++------
 test/bundler/bundler_barrel.test.ts | 112 +++++++++++++++++++++++++
 2 files changed, 250 insertions(+), 24 deletions(-)

gate history · 4 passed · 0 rejected · iteration 1

evidence per changed file
file                                 reads  edits  tests
src/bundler/barrel_imports.rs           10     34      0
test/bundler/bundler_barrel.test.ts      2      4      0

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…

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

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Changes

Barrel namespace propagation

Layer / File(s) Summary
Target resolution and BFS scheduling
src/bundler/barrel_imports.rs
Shared target resolution is used for bare imports, dynamic imports, and export * BFS scheduling.
Namespace request propagation
src/bundler/barrel_imports.rs
Deferred records resolve during propagation. Full-namespace and aliased requests pass through named and star re-exports. Repeated All requests are skipped.
Chained barrel regression coverage
test/bundler/bundler_barrel.test.ts
Adds coverage for namespace imports through chained barrels, code splitting, emitted output, and runtime values.

Possibly related PRs

  • oven-sh/bun#36549: Both changes update barrel import path and source-index resolution.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address issue #36832 by propagating barrel requests, stabilizing output, preventing cycles, and adding a regression test.
Out of Scope Changes check ✅ Passed The source changes and regression test directly support the linked issue and stated pull request objectives.
Title check ✅ Passed The title clearly and concisely describes the primary change: namespace request propagation through chained barrels.
Description check ✅ Passed The description includes both required sections and provides detailed context, cause, fix, and verification results.

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

Comment thread src/bundler/barrel_imports.rs Outdated
Comment thread src/bundler/barrel_imports.rs Outdated
Comment thread src/bundler/barrel_imports.rs Outdated
Comment thread src/bundler/barrel_imports.rs Outdated
Comment thread src/bundler/barrel_imports.rs Outdated
Comment thread src/bundler/barrel_imports.rs
Comment thread src/bundler/barrel_imports.rs
Comment thread src/bundler/barrel_imports.rs

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

📥 Commits

Reviewing files that changed from the base of the PR and between 074656d and 68ca276.

📒 Files selected for processing (2)
  • src/bundler/barrel_imports.rs
  • test/bundler/bundler_barrel.test.ts

Comment thread src/bundler/barrel_imports.rs

@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/barrel_imports.rs (1)

785-786: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Resolve nested barrel targets with the DevServer path fallback.

DevServer records can have an invalid rec.source_index; Lines 452-476 already handle this with path_to_source_index_map. These new guards skip both named re-exports and export * 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

📥 Commits

Reviewing files that changed from the base of the PR and between 68ca276 and a134840.

📒 Files selected for processing (1)
  • src/bundler/barrel_imports.rs

Comment thread src/bundler/barrel_imports.rs Outdated
@robobun

robobun commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

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.

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

📥 Commits

Reviewing files that changed from the base of the PR and between a134840 and 1b688b8.

📒 Files selected for processing (1)
  • src/bundler/barrel_imports.rs

Comment thread src/bundler/barrel_imports.rs Outdated
Comment thread src/bundler/barrel_imports.rs
Comment thread src/bundler/barrel_imports.rs
Comment thread src/bundler/barrel_imports.rs

@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/barrel_imports.rs (1)

743-743: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use usize in un_defer_record. import_record::List uses usize lengths, and no u32 bound is declared. Passing idx directly 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8ae8bf7 and 9e9bec9.

📒 Files selected for processing (1)
  • src/bundler/barrel_imports.rs

Comment thread src/bundler/barrel_imports.rs
@robobun

robobun commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

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.

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

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.

@Jarred-Sumner
Jarred-Sumner merged commit 9b99e7c into main Aug 3, 2026
54 of 55 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/5b28b337/fix-barrel-splitting-nondeterminism branch August 3, 2026 20:23
springmin pushed a commit to springmin/bun that referenced this pull request Aug 3, 2026
…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>
Jarred-Sumner pushed a commit that referenced this pull request Sep 14, 2026
### 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>
usrbinkat pushed a commit to usrbinkat/bun that referenced this pull request Sep 15, 2026
…-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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants