Skip to content

dev server: keep a barrel's requests across builds, or never defer it again - #46648

Open
robobun wants to merge 5 commits into
mainfrom
robobun/761f31d9/dev-server-barrel-requests-across-builds
Open

robobun wants to merge 5 commits into
mainfrom
robobun/761f31d9/dev-server-barrel-requests-across-builds

Conversation

@robobun

@robobun robobun commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • In the dev server, a page reads an export of a "sideEffects": false barrel. After another route builds, it reads undefined (r2 B, then r2 undefined).
  • Each build parses only new and changed files. A re-parsed barrel kept only the names in DevServer.barrel_needed_exports, which only named imports wrote (src/bundler/barrel_imports.rs:561). Requests for every export or through an outer barrel were lost.

Fix

  • TrackedBarrels replaces the two DevServer maps: one entry per graph side and path. Each build adds its requests once (persist_requests_of_tracked_barrels).
  • An entry with a request for every export never defers again, nor does a bundled barrel with no entry.
  • Correct: a barrel defers only when it is new to the graph, or its entry has every earlier request.
  • Verified: 18 new tests in test/bake/dev/bundle.test.ts (12 fail on main), 2 unit tests. Self-reviewed: 46 concerns raised, 41 addressed. Notes name the rest.

Background

  • A barrel is a file that only re-exports. The bundler defers (skips) those that nothing requests.
  • A path can have several source indices per build. SourcesByPath finds them.
  • Considered mirroring each place that records a request, as main does for named imports: a cached barrel has none.

Downsides

Notes

Origin. A fuzz run of dev server sequences found the shape below. No user reported this exact shape. It is the second "Not changed" bullet of #46229.

node_modules/lib/package.json  { "sideEffects": false }
node_modules/lib/index.js      export { a } from "./a.js"; export { b } from "./b.js";
r1.ts  import { a } from "lib"
r2.ts  import { b } from "./mid"      mid.ts  export * from "lib"
r3.ts  import { a } from "lib"
/r1 -> r1 A | /r2 -> r2 B | /r3 -> r3 A | /r2 -> r2 undefined      (1.4.0, 1.4.3, main)

New tests that fail on main (release build of main 179664e):

Test (barrel optimization: ...) main
a later build keeps a request for every export (export *, export * as, import * as, import(), require()) whole: undefined undefined undefined undefined undefined
a later build keeps a name requested through an outer barrel outer: OWN undefined
a save of one importer keeps the requests of the other importers saved: A undefined
a later build keeps a name that an onResolve plugin let pass (the persist_barrel_export key defect from the review of #45248) first: undefined
a barrel that was not there to read keeps the requests of that build the client bundle of /whole has no WHOLE_B
a later build of server code keeps a request for every export whole: undefined
a rebuild of a failing route keeps the requests made while the barrel was cached named: undefined
a barrel that fails to parse keeps the request for every export of that build whole: undefined
a copy that one side loaded in full stays so when the other side defers (one server graph, separate SSR graph) whole: undefined
a later build keeps a request for every export of a "use client" barrel the client bundle has no EXPORT_C
the SSR copy keeps a request for every export when a file of the barrel moves page: EXPORT_A

Six more new tests pass on main. They pin what this design must keep: a saved barrel defers again in the next build, a barrel that is parsed again still defers what nothing requested (server copy and SSR copy), a loaded outer barrel gets the exports that a later build loads (server, and an open page), and a name requested from a "use client" barrel survives a later build.

How the state moves.

  • TrackedBarrels has one map per side of the dev server's module graph. The server and SSR copies of a file share one graph record and one list of imports, so they share one entry.
  • No entry, and the side has not bundled the file: the barrel can defer. If it does, it gets a Partial entry.
  • An entry on either side makes is_file_cached return None. Every build that reaches the barrel parses it, as on main. Partial: the names of the entry count as requested. All: the barrel returns before the deferral loop.
  • finish_from_bake_dev_server calls persist_requests_of_tracked_barrels once. It adds each slot of requested_exports to the entry of its path, on the side of the copy that has that source index. A request for every export makes the entry All.
  • No entry, and the side has bundled the file: the barrel returns before the deferral loop. Builds took it from the cache and recorded no request for it.

Design notes.

  • A barrel with an All entry is still parsed and sent in every build that reaches it. A re-export in a node_modules file, and every export *, is a value that the importer copies when it is evaluated. Only a new copy of an outer barrel gets the exports that an inner barrel loads later. A version that cached such a barrel left undefined in a loaded outer barrel until restart.
  • The end-of-build step reads a request under the source index that the importer recorded it for. The path maps do not lead back to that copy: the bundler points the path of a client component boundary at the copy it generates, and a file that was not there to read gets a second index. A request under the generated copy of a boundary goes to the entry of the original file.
  • The barrel pass also counts the requests under another source index of its own path and side. SourcesByPath finds those indices: a map from the hash of a path to its newest source index, and for each source the next older index with the same hash. Each build adds only its new sources. The first version of this PR walked every slot of requested_exports for each barrel (CPU table below).
  • A copy that failed to open has no side of its own. While a path map still points at it, that map gives the side. A copy with a graph record is removed from the path maps (on_file_deleted), so its requests count for both sides.
  • The pass has one more scan per file, graph.entry_points.iter().any(..). It is main's code and is left as it is.
  • has_file for the server side is true only for a record that a build bundled (kind Js). A route file has a record of unknown kind from startup.
  • finalize_bundle has a debug_assert!: a copy with a deferred record has an entry.

Downsides in detail.

  • Ways to the never-defer state: one request for every export (in the dev server import() and require() are such requests), a first build that took the barrel whole, a first build in which the file was not a pure barrel, and a first build of the client copy that failed. No save clears it. main deferred again on the next build that parsed the barrel.
  • A failing re-export, 12 cases (unresolved import, syntax error, throw at evaluation, each with import *, import(), require(), export *): after the importer goes back to a named import, main prints the value. This branch shows the error of that file on every page that imports the barrel, until the server restarts.
  • A re-exported file that is renamed before any build read it (c.js to c.jsx). The resolver still names the old path from its cached listing of the directory, and the read fails. On main only the page that requests that export gets the dead import. On this branch, after a request for every export, each page that imports the barrel gets it (server code: 500), until restart. The stale listing is a defect of main and has no fix there yet. dev server: read a directory again when a file that no build bundled fails to open #46846 is a draft for it: its prototype reads the directories again in the next build, and it waits for bundler: advance the resolver generation once per build instead of once per queue drain #38212 and resolver: keep cached DirEntry when a stale-generation refresh fails #36874. Two tests for this case (client and server code) pass on robobun/761f31d9/barrel-requests-with-generation-prototype, which has this PR and that prototype (84 tests in the file, debug build). They fail on this branch alone, so they are not in this PR.
  • Not changed from main: requests are never removed, and a barrel with an entry is sent again by every build that reaches it. Importers that accept the update run again. A page with no accept boundary above the barrel reloads, also when the build is the first build of another route.

Still open after this PR (each one is the same on main):

  • The first "Not changed" bullet of dev server: an export * after another statement of the same path loses exports of a barrel target #46229. Two statements of one path plus a late request still print got: C undefined in the first build. The branch robobun/fd59aa7e/hmr-merged-import-record works on it and changes the same function.
  • Reload order of a hot update. A module that copies exports by value can copy from a dependency that the same update has not reloaded yet. The 35 undefined reads that the fuzz still finds on this branch are of this kind, where inspected. Serve fresh exports when CommonJS modules hot-reload in the dev server #31944 marks every module of an update stale before it reloads any.
  • The 5 self-review concerns that this PR does not address: an off switch for barrel deferral (3 concerns, bundler: turn automatic barrel mode off for --ignore-dce-annotations and for the bun test --changed scan #45938 adds the guard that such a switch needs), and the conditions for a later change that caches a barrel with an All entry (2 concerns, it needs the reload order change first).
  • A second review looked at the lookup change and at the moved-file case. It raised 7 concerns. Two belong to this PR and are addressed: two unit tests for SourcesByPath, and the side rule for a failed copy in the design notes. The other five are about the stale listing: they went to dev server: read a directory again when a file that no build bundled fails to open #46846 and to separate reports (an importer that on_file_deleted skips because it no longer resolves, a package directory that is a symlink).
  • A barrel whose first parse fails. The barrel has no entry yet, so the requests that its importers made in that build are not kept. On the server side the fixed barrel can then defer a record that a page of that build reads, as on main. On the client side it never defers again, because has_file counts a failure record. That part is new.
  • One server graph and a "use client" barrel: a server file that requests a name after the barrel is parsed, in the same build, reads undefined until the next build. The new boundary loop of persist_requests_of_tracked_barrels corrects only the next build.
  • TrackedBarrels::memory_cost has no test.
  • bundler: keep a barrel's import record when a statement of the barrel reads a binding from it #45538 changes the same function. It merges with this branch without a conflict in barrel_imports.rs. The appended tests conflict.

Measurements (main 5c6d4e2 against the first version of this branch, release builds, if a line names no other commit). Fixture A: a page with 50 named imports and no barrel. Fixture C: a page that imports one name from each of 20 barrels. Fixture L: import { Camera } from 'lucide-react' (1.61.0).

  • Release text (llvm-size, main 179664e): 107,556,528 -> 107,564,208 bytes (+7,680). Before SourcesByPath it was +4,608. DevServerHandle link slots 11 -> 10.
  • bun build of fixture A, schedule_barrel_deferred_imports (gdb stepi): 34,949 -> 34,011 instructions for the file with 50 named imports, 2,722 -> 2,705 for each imported module.
  • is_file_cached per call (gdb stepi): no tracked barrel 206 -> 160 (file not in the graph) and 273 -> 226 (cached file). With 20 tracked barrels 188 -> 219 and 265 -> 301.
  • End-of-build step per hot rebuild (gdb stepi): A 16 -> 24 instructions, L 16 -> 517, C 16 -> 8,897.
  • Allocator calls inside on_parse_task_complete, first build (gdb): A 741 -> 650, C 2,678 -> 2,604 plus 60 in the end-of-build step.
  • JS-thread instructions per hot rebuild (perf_event_open, interleaved runs): A median 740,526 -> 754,920 with a spread of 679k to 926k. C 1,732,934 -> 1,779,827 with a spread of 1.59M to 1.86M. L 11.73M -> 12.34M with a spread of 11.0M to 13.8M. Each difference is inside the spread.
  • Peak RSS after 25 saves (medians): A 43,696 -> 43,768 kB, C 46,764 -> 46,780 kB, L 98,048 -> 97,748 kB.
  • Save to served, median of 25 saves, 4 interleaved runs: A 4.3 to 6.4 ms -> 4.1 to 7.3 ms. C 5.7 to 6.6 -> 5.6 to 6.0 ms. L 16.1 to 25.0 -> 17.1 to 23.5 ms.
  • lucide-react, /named has import { Camera } and /whole has import * as: /named ships 622,618 bytes in 16 modules at first, and 3,285,979 bytes in 1,919 modules after /whole is built, on both. After whole.ts goes back to a named import, main ships 622,618 bytes and this branch 3,285,979 bytes.
  • The container has no perf, valgrind, bloaty or strace. The numbers come from gdb, nm, llvm-size and a perf_event_open wrapper.

CPU of the barrel pass at scale. A page that imports one name from each of N two-line sideEffects: false barrels. CPU ms (process.cpuUsage, user plus system) of the first build and of a save of the page, release builds, 3 interleaved runs, median in parentheses. main is 179664e. "walk" is this PR before SourcesByPath (248f5e3). The first run of main in each group reads the files cold.

barrels build main walk this branch
4,000 first 558/358/485 (485) 519/487/531 (519) 343/351/346 (346)
4,000 save 186/192/185 (186) 235/234/262 (235) 191/178/241 (191)
8,000 first 5505/631/675 (675) 1348/1278/1167 (1278) 641/768/700 (700)
8,000 save 419/346/346 (346) 620/643/656 (643) 355/357/353 (355)
16,000 first 10784/1511/1422 (1511) 4133/4816/4168 (4168) 1356/2995/1586 (1586)
16,000 save 827/862/820 (827) 2037/2368/2099 (2099) 1060/951/798 (951)
5,000, no sideEffects first 4090/562/523 (562) 560/552/528 (552) 566/493/633 (566)
5,000, no sideEffects save 70/69/76 (70) 73/81/80 (80) 77/73/85 (77)

The bytes served are equal on all three. The lookup is pinned by a barrel that was not there to read keeps the requests of that build (it fails if the lookup returns nothing) and by two unit tests in barrel_imports.rs (the other indices of a path after a second indexing pass, and two paths with one hash). CI runs the unit tests under cargo miri test -p bun_bundler.

Fuzz. Random sequences of route loads, saves and failing routes over barrels, 356 seeds in 12 modes (HTML routes, server graph, one server graph with a client entry, separate SSR graph, each with and without nested barrels, "use client" barrels and app-level barrels). The two release builds ran the same seeds side by side.

  • Seeds with an undefined read: 56 on main, 14 on this branch. No seed fails only on this branch.
  • Distinct undefined reads: 169 on main, 35 on this branch. 34 of the 35 are on main at the same step. The other one depends on timing: in 6 reruns main has it 6 times and this branch 2 times.
  • 110 of the seeds also ran on the debug build. The new debug_assert! did not fire, and the results match the release build.

Suites run on the debug build of f070caf: test/bake/dev/bundle.test.ts (72), test/bundler/bundler_barrel.test.ts (70), test/bake/dev/{css,ecosystem,esm,harness,hot,html,import-meta-inline,import-meta-inline-negative,incremental-graph-edge-deletion,plugins,react-response,request-cookies,server-sourcemap,sourcemap,ssg-pages-router,stress,vfile}.test.ts, test/bake/{app-options,deinitialization,dev-and-prod,framework-router,hmr-socket-protocol,serve-plugins-dev-server}.test.ts. On a release build: bundle.test.ts 3 times, its 26 barrel tests 10 times, test/bake/dev/production.test.ts (21) and react-spa.test.ts (10). On the debug build 10 tests of production.test.ts and 1 of react-spa.test.ts reach the 5 s limit. They pass on release builds of this branch and of main.


[human-review] gate passed · iteration 1 · 10 files touched

fails on main (without fix)
ASAN without fix: 12 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bake/dev/bundle.test.ts
bun test v1.4.4 (367d939d9)

test/bake/dev/bundle.test.ts:
Dev server testing directory: /tmp/bun-dev-test-ggWCLg
^[[0;30mdev|^[[0m Started development server: http://localhost:39771
^[[0;30mdev|^[[0m ^[[32mBundled page in 393ms^[[0m^[[2m:^[[0m routes/index.ts ^[[2m+ 1 more^[[0m
^[[0;30mdev|^[[0m [Bun] Hot update was not accepted because it or its importers do not call `import.meta.hot.accept`. To prevent full page reloads, call `import.meta.hot.accept` in one of the following files to handle the update:
^[[0;30mdev|^[[0m 
^[[0;30mdev|^[[0m Module "db.ts" is not accepted by routes/index.ts,
^[[0;30mdev|^[[0m ^[[32mReloaded in 83ms^[[0m^[[2m:^[[0m db.ts
^[[0;30mdev|^[[0m [Bun] Hot update was not accepted because it or its importers do not call `import.meta.hot.accept`. To prevent full page reloads, call `import.meta.hot.accept` in one of the following files to handle the update:
^[[0;30mdev|^[[0m 
^[[0;30mdev|^[[0m Module "routes/index.ts" is a root module that does not self-accept.
^[[0;30mdev|^[[0m ^[[36m[x2]^[[0m ^[[32mReloaded in 54ms^[[0
... (truncated)

release without fix: all passed
bun test v1.4.4-canary.1 (feb6b21fa)

test/bake/dev/bundle.test.ts:
Dev server testing directory: /tmp/bun-dev-test-s22NNg
^[[0;30mdev|^[[0m Started development server: http://localhost:34013
^[[0;30mdev|^[[0m ^[[32mBundled page in 187ms^[[0m^[[2m:^[[0m routes/index.ts ^[[2m+ 1 more^[[0m
^[[0;30mdev|^[[0m [Bun] Hot update was not accepted because it or its importers do not call `import.meta.hot.accept`. To prevent full page reloads, call `import.meta.hot.accept` in one of the following files to handle the update:
^[[0;30mdev|^[[0m 
^[[0;30mdev|^[[0m Module "db.ts" is not accepted by routes/index.ts,
^[[0;30mdev|^[[0m ^[[32mReloaded in 4ms^[[0m^[[2m:^[[0m db.ts
^[[0;30mdev|^[[0m [Bun] Hot update was not accepted because it or its importers do not call `import.meta.hot.accept`. To prevent full page reloads, call `import.meta.hot.accept` in one of the following files to handle the update:
^[[0;30mdev|^[[0m 
^[[0;30mdev|^[[0m Module "routes/index.ts" is a root module that does not self-accept.
^[[0;30mdev|^[[0m ^[[36m[x2]^[[0m ^[[32mReloaded in 3ms^[[0m^[[2m:^[[0m routes/index.ts
(pass)  DEV:bundle-1: import identifier doesnt get renamed [1039.22ms]
^[[0;30mdev|^[[0m Started development server: http://local
... (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/pr_gate.xml" test/bake/dev/bundle.test.ts
rustup spent 9s installing the pinned toolchain (nightly-2026-09-15); it was missing or incomplete on this machine
bun test v1.4.4 (367d939d9)

test/bake/dev/bundle.test.ts:
Dev server testing directory: /tmp/bun-dev-test-bXbpEz
^[[0;30mdev|^[[0m Started development server: http://localhost:44743
^[[0;30mdev|^[[0m ^[[32mBundled page in 573ms^[[0m^[[2m:^[[0m routes/index.ts ^[[2m+ 1 more^[[0m
^[[0;30mdev|^[[0m [Bun] Hot update was not accepted because it or its importers do not call `import.meta.hot.accept`. To prevent full page reloads, call `import.meta.hot.accept` in one of the following files to handle the update:
^[[0;30mdev|^[[0m 
^[[0;30mdev|^[[0m Module "db.ts" is not accepted by routes/index.ts,
^[[0;30mdev|^[[0m ^[[32mReloaded in 64ms^[[0m^[[2m:^[[0m db.ts
^[[0;30mdev|^[[0m [Bun] Hot update was not accepted because it or its importers do not call `import.meta.hot.accept`. To prevent full page reloads, call `import.meta.hot.accept` in one of the following files to handle the update:
^[[0;30mdev|^[[0m 
^[[0;30mdev|^[[0m Module "
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 16816ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/112] gen generated_host_exports.rs
generated_host_exports.rs: 111 exports (lazy=4, generic=107, rust=0); 255 extern-C blocks audited
[2/111] build.rs build_script_build
[3/111] rustc bun_platform 
[4/111] rustc bun_core 
[5/111] rustc bun_safety 
[6/111] rustc bun_zlib_sys 
[7/111] rustc bun_zstd 
[8/111] rustc bun_output 
[9/111] rustc bun_ptr 
[10/111] rustc bun_brotli 
[11/111] rustc bun_boringssl_sys 
[12/111] rustc bun_lsquic_sys 
[13/111] rustc bun_cares_sys 
[14/111] rustc bun_picohttp 
[15/111] rustc bun_base64 
[16/111] rustc bun_clap 
[17/111] rustc bun_errno 
[18/111] rustc bun_tcc_sys 
[19/111] rustc bun_text_diff 
[20/111] rustc bun_valkey 
[21/111] rustc bun_shell_parser 
warning: `feature(generic_const_exprs)` is not supported with the next-generation trait solver
 --> src/shell_parser/lib.rs:1:30
  |
1 | #![feature(adt_const_params, generic_const_exprs, allocator_api)]
  |                              ^^^^^^^^^^^^^^^^^^^
  |
  = note: `-Znext-solver=globally` is currently enabled by de
... (truncated)
diff hotspot
src/bundler/barrel_imports.rs                 | 220 +++++++++--
 src/bundler/bundle_v2.rs                      |  11 +
 src/bundler/lib.rs                            |   5 +-
 src/runtime/bake/dev_server/bundle.rs         |  17 +-
 src/runtime/bake/dev_server/bundler_vtable.rs |  17 +-
 src/runtime/bake/dev_server/finalize.rs       |  13 +
 src/runtime/bake/dev_server/lifecycle.rs      |   6 +-
 src/runtime/bake/dev_server/memory_cost.rs    |   7 +-
 src/runtime/bake/dev_server/mod.rs            |  11 +-
 test/bake/dev/bundle.test.ts                  | 505 ++++++++++++++++++++++++++
 10 files changed, 748 insertions(+), 64 deletions(-)

gate history · 1 passed · 1 rejected · iteration 1

evidence per changed file
file                                           reads  edits  tests
src/bundler/barrel_imports.rs                      7      3     56
src/bundler/bundle_v2.rs                           1      1     57
src/bundler/lib.rs                                 1      0     56
src/runtime/bake/dev_server/bundle.rs              1      2     56
src/runtime/bake/dev_server/bundler_vtable.rs      0      0     56
src/runtime/bake/dev_server/finalize.rs            2      1     56
src/runtime/bake/dev_server/lifecycle.rs           0      0     57
src/runtime/bake/dev_server/memory_cost.rs         0      0     56
src/runtime/bake/dev_server/mod.rs                 0      0     56
test/bake/dev/bundle.test.ts                       4      2     56

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between e3c34fe and f070caf.

Files selected for processing: 5
  • src/bundler/barrel_imports.rs
  • src/bundler/bundle_v2.rs
  • src/runtime/bake/dev_server/bundle.rs
  • src/runtime/bake/dev_server/finalize.rs
  • src/runtime/bake/dev_server/mod.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

Walkthrough

The change replaces separate dev-server barrel maps and per-alias callbacks with side-aware tracked barrel state. Builds merge requests into tracked entries and use graph-side cache presence when deciding whether to defer records. Tests cover request retention across client, server, and SSR builds.

Changes

Side-aware deferred barrel request tracking

Layer / File(s) Summary
Track and merge barrel requests
src/bundler/barrel_imports.rs
Adds per-side tracked request storage and source-path indexing. Barrel optimization combines current and persisted requests, and build completion persists requests for tracked barrels. Removes per-alias persistence.
Connect tracked requests to dev-server builds
src/bundler/bundle_v2.rs, src/bundler/lib.rs, src/runtime/bake/dev_server/*
Adds graph-side mapping and build-time request persistence. Replaces the separate dev-server maps and callbacks with TrackedBarrels and has_file. Updates cache checks, memory accounting, and a debug assertion.
Test request retention across builds
test/bake/dev/bundle.test.ts
Adds client, server, and SSR regression tests for retained requests, deferred exports, and graph-specific barrel copies.

Possibly related PRs

  • oven-sh/bun#46742: Changes how HMR lowering preserves import records, which barrel optimization uses.

Suggested reviewers: alii, jarred-sumner

Priority: Normal

Merge Risk: Minimal · up to f070c

No actionable correctness or availability issue was established. The change is ready to merge after normal checks, with the documented bundle-size tradeoff.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly summarizes the main change: preserving barrel requests across dev-server builds and preventing incorrect deferral.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description is complete and relevant. It explains the problem, fix, design, tradeoffs, known limitations, and verification results. It does not use the template headings exactly, but it provides t…
Full details: Description check

Explanation

The description is complete and relevant. It explains the problem, fix, design, tradeoffs, known limitations, and verification results. It does not use the template headings exactly, but it provides the required information through equivalent sections such as “Problem,” “Fix,” and “How the state moves.”

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@robobun

robobun commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 2:55 AM PT - Oct 25th, 2026

✅ @robobun, your commit f070caf032d50dbb6b6c5772dfc141e7f4a2e3b7 passed in Build #132501! 🎉


🧪   To try this PR locally:

bunx bun-pr 46648

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

bun-46648 --bun

@robobun

robobun commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

How this was reproduced. A dev server (Bun.serve HTML routes, development: { hmr: true }) on main 5c6d4e2, with a package lib that has "sideEffects": false and index.js = export { a } from "./a.js"; export { b } from "./b.js";:

  • /r1 imports { a } from lib.
  • /r2 imports { b } from ./mid, and mid.ts is export * from "lib".
  • /r3 imports { a } from lib.

The sequence /r1, /r2, /r3, /r2 prints r2 B and then r2 undefined. On main, 12 of the 18 new tests in test/bake/dev/bundle.test.ts fail (-t "barrel optimization"). On this branch all 26 barrel tests pass.

PR: #46648

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 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/runtime/bake/dev_server/finalize.rs Outdated
Comment thread src/runtime/bake/dev_server/mod.rs Outdated

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread src/bundler/barrel_imports.rs
Comment thread src/runtime/bake/dev_server/bundle.rs
Comment thread src/runtime/bake/dev_server/bundle.rs
Comment thread src/bundler/barrel_imports.rs
@robobun

robobun commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

A prototype of the follow-up that "Still open" names (do not parse and send a barrel again on each save of an importer) is on the branch robobun/bdc0b481/dev-barrel-cache-answer. It is not open as a PR.

What it does: is_file_cached answers for a barrel that has deferred re-exports. It misses only when the import reads a deferred name.

Measured on release builds, main 5c6d4e2 against the branch. JS bytes in the hot update for one save of a file that imports the barrel:

Barrel main branch
lucide-react 1.61.0 512,459 572
lodash-es 4.17.21 26,783 397

Why it is not a PR:

  • It needs the reload order change of Serve fresh exports when CommonJS modules hot-reload in the dev server #31944 first. Without the resend, a wrong first read through two nested node_modules barrels stays undefined until a page reload. On main the next saves repair it.
  • It has its own rule to keep what is live. It must use the request table of this PR, not a second one.
  • It needs dev server: keep one import record for each import statement #46742 for a barrel with two statements of one path.
  • The prototype gives a wrong answer in three cases that have no test yet: a saved barrel gains an export that an importer already names, a re-export fails and the full read is then removed, and a server file imports a "use client" barrel with separateSSRGraph.

… again

A dev server build parses only the new and the changed files. A barrel that
it parsed again chose its deferred records from the requests of that build
and from a set of export names that only named imports wrote. A request for
every export (import * as, export * from, require(), import()), a name that
reached an inner barrel through an outer barrel, and a request made while
the barrel was cached were lost. A later build then deferred a record that
a module of an earlier build still reads, and that module read undefined.

TrackedBarrels replaces the two DevServer maps. It has one entry per side
of the module graph and per path, for each barrel that has deferred a
record. Such a barrel is parsed in every build that reaches it, as before.
When every file of a build is parsed, the requests of that build are added
to the entries, each under the source index that its importer recorded it
for. An entry that got a request for every export never defers again. A
barrel that the dev server has bundled and does not track never defers.
The test removed two barrels that one module imports and then built a page
that reads both. The dev server loads that importer again when the first
barrel fails to open. Whether the second barrel gets a new copy in the same
build depends on when its failure arrives, on main too. The test now removes
one barrel for each build and reads the client bundle of each page.
Keep main's comment at the dev server block of the barrel pass and at the
re-parse check of is_file_cached.
…y a walk

The barrel pass looked for requests under another source index of its own
path by a walk over every request slot of the build. With N barrels in one
build that is N walks over about 3N slots. It now keeps the source indices
of the build by the hash of their path and reads one chain per barrel.
SourcesByPath: the other indices of a path, and two paths with one hash.
@robobun
robobun force-pushed the robobun/761f31d9/dev-server-barrel-requests-across-builds branch from 248f5e3 to f070caf Compare October 25, 2026 08:42

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

Code review found no issues

No high-confidence issues detected in this change.

@robobun

robobun commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator Author

This comment is replaced by #46648 (comment). #46944 is closed.

@robobun

robobun commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

#46944 was the follow-up that targeted this branch. I closed it. This comment has what that work found about this PR.

This branch alone answers worse than main. The barrel tests of #46944 (bun test test/bake/dev/bundle.test.ts -t "barrel optimization", 67 tests, release builds):

main a8fcd31 this branch f070caf with commit 1 of #46944 with commits 1 and 2
pass of 67 32 34 57 67
  • 15 tests pass on main and fail on this branch. Three of them are plain Bun.serve HTML routes, with no app option.
  • The cause: main writes a named request when the importer records it. This branch writes the requests at the end of the build, and only to the entry of a barrel that deferred a record. A barrel whose first build fails has no entry. After the repair its importers read undefined, or the barrel never defers again.
  • The "Still open" section of this PR says that this case is the same as on main. For those 15 tests it is not.

Why #46944 is closed. Commit 1 kept the requests of a failed build in a second table of TrackedBarrels. Commit 2 kept each request with its importer and removed it when a build bundled the importer again. Commit 2 is on the branch robobun/f28cf70d/barrel-requests-by-importer-prototype and was never in the PR. Each repair needed another rule, and a review of the two commits still found these defects:

  • Commit 1: a kept name has no owner. If it names a re-export that cannot build, every route of the barrel answers 500 until restart, also after the importer drops the name (dev server: keep what a build requests from a barrel that it does not bundle #46944 (comment)). For a named import, main does the same. For a request for every export (import *, require(), import()), main forgets the request and answers 200.
  • Commit 1: two routes share one bundled importer of a failed barrel. The second route answers Failed to load bundled module 'ui/index.ts'. This is not a dynamic import, and therefore is a bug in Bun's bundler. Main and this branch answer 200.
  • Commit 1: an HTML route, and a re-export that fails to resolve after an onResolve filter matched it. After the repair, the script of a page lacks two of the three exports that the page imports. Main and this branch have all three.
  • Commit 2: it finds the importers by a second pass over the import records. That pass can give a request to the wrong importer. With a barrel that is missing for one build and an outer barrel that re-exports from it, an importer then reads undefined. Commit 1, this branch and main are right there.
  • Commit 2: the server removes a request when it bundles the importer again. An open page can still hold the old importer.
  • Both commits: of 12,046 probed sequences of 5 steps with importer edits, 1,069 are right on main and answer 500 with both commits. 556 are wrong on main and right with both commits. A 200 that prints undefined is in 225 of the sequences on main, in 153 on this branch, and in none with the two commits.

Two decisions that this PR needs from a maintainer. More code in TrackedBarrels does not settle them.

  1. Where do the names that an importer requests from a barrel live? The options: on the edge of the importer in IncrementalGraph (written in process_edge_attachment, replaced by the import diff, freed with the edge), in a record that schedule_barrel_deferred_imports writes for each import, or in TrackedBarrels as now. The edge needs one graph record for each server-side graph first.
  2. A bundled file requests an export that cannot build. Which answer is wanted: a build error on every route of that barrel, or the 200 of main with undefined in that one file?

What can be reused. The prototype branch has both commits and 41 devTests in test/bake/dev/bundle.test.ts (28 for commit 1, 13 for commit 2). The body of #46944 has the result of each test on four builds and the measurements.

Until those decisions are made, do not merge this PR alone. It fails the 15 tests above that main passes.

This branch has not been deployed

No deployments
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.

2 participants