Skip to content

dev server: an export * after another statement of the same path loses exports of a barrel target - #46229

Merged
dylan-conway merged 3 commits into
mainfrom
robobun/c4617095/hmr-export-star-surviving-record
Oct 22, 2026
Merged

dylan-conway merged 3 commits into
mainfrom
robobun/c4617095/hmr-export-star-surviving-record

Conversation

@robobun

@robobun robobun commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes a regression in 1.4.3, from #42737. The dev server (Bun.serve HTML routes) gives the app undefined for an export, with no build error, when a module has an export * after another statement of the same path and the target is a "sideEffects": false barrel:

// node_modules/lib/index.js
export { a } from "./inner.js";
export * from "./inner.js";

// node_modules/lib/inner.js
export { a } from "./a.js";
export { b } from "./b.js"; // b.js is not served: `import { b } from "lib"` gives undefined

The first statement can also be an import, and the module with the export * can be app code that re-exports a barrel package. @mui/material 7 has this shape in esm/index.js (export { default as RadioGroup } from "./RadioGroup/index.js"; export * from "./RadioGroup/index.js";), so a root import of useRadioGroup is undefined.

Cause

  • HMR lowering (deduplicated_import in src/js_parser/lower/lower_esm_exports_hmr.rs) merges every statement of one path into the first one. It marks the import record of each later statement IS_UNUSED, so that record is never resolved.
  • export_star_import_records still listed that record. on_parse_task_complete and the barrel pass (src/bundler/barrel_imports.rs) take the target of an export * from this list. They found no target, so nothing asked the target for all of its exports, and the target deferred every re-export that no importer named.
  • Before bundler: un-defer only the records barrel optimization deferred #42737, un_defer_record cleared IS_UNUSED on any record. That also revived the merged-away record, and the target was found through it. bundler: un-defer only the records barrel optimization deferred #42737 limits it to the records the barrel pass deferred.

Fix

With HMR lowering, convert_stmt pushes the record that the merged statement uses (deduped.import_record_index) to export_star_import_records. ImportScanner pushes the statement's own record only without HMR lowering. Every reader of the list now gets a resolved record, including the stars list the dev server prints for each module.

Not changed

Both of these are the same on 1.4.2 and on this branch:

  • named_imports still names the merged-away record of an export { b } from or export * as ns from. The name is undefined when it is first requested after the barrel was parsed and its path was deferred.
  • A request for every export is not kept across incremental builds. A barrel that deferred a re-export before an export * asked for all of it defers it again on the next rebuild.

How did you verify your code works?

New test in test/bake/dev/bundle.test.ts: barrel optimization: export star after another statement of the same path.

1.4.2 1.4.3 main 491aaad1 this PR
new test pass fail fail pass
root import of @mui/material 7.3.11 in Chromium: useRadioGroup, ModalManager, ButtonGroupContext, stackClasses defined undefined defined
  • Without the fix the test prints got: undefined function undefined undefined undefined. The one function is the control, where the export * is the first statement of its path.
  • The test passes with USE_SYSTEM_BUN=1 on a system Bun of 1.4.2 or older, which does not have the regression.
  • All of test/bake/dev/bundle.test.ts passes on the debug build.

Generated by Claude Code

… HMR lowering

HMR lowering merges every statement of one import path into the first
one and retires the import record of each later statement. A retired
record is never resolved. export_star_import_records kept the retired
record of a merged `export *`, so the barrel pass never found the
target of that `export *`: the target was requested only for the names
of the other statement, and a target that is a barrel deferred the
rest. Those exports were undefined in the dev server.

convert_stmt now writes the record that the merged statement uses into
the entry that ImportScanner pushed for the statement. named_imports
has the same stale index and is not changed here.
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: d9197290-8fb1-4537-9ba3-4082139344a5
📥 Commits

Reviewing files that changed from the base of the PR and between 491aaad and 8a00416.

📒 Files selected for processing: 2
  • src/js_parser/lower/lower_esm_exports_hmr.rs
  • test/bake/dev/bundle.test.ts

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

Walkthrough

When export * from imports are deduplicated, the tracker now points to the surviving import record. A regression test covers merged barrel imports and re-export chains.

Changes

Export-star deduplication

Layer / File(s) Summary
Track and test the surviving record
src/js_parser/lower/lower_esm_exports_hmr.rs, test/bake/dev/bundle.test.ts
The tracker is updated to the surviving import record after deduplication. Regression coverage checks named exports across merged barrels, export-star ordering, nested re-exports, and a large source file.

Suggested reviewers: jarred-sumner

Priority: ⬆️ High

Merge Risk: ⚪ Minimal · up to 8a004

The export-star tracking fix and regression coverage show no identified issue that should delay merging, subject to normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
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.
Title check Passed The title clearly identifies the dev server regression and the specific export-star scenario that causes barrel exports to be lost.
Description check Passed The description includes both required sections. It explains the regression, cause, fix, scope limitations, and verification results with specific test details.
✨ Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • 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 20, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:05 PM PT - Oct 21st, 2026

@claude, your commit 87492c1 is building: #130451

@robobun

robobun commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on 1.4.3 and on main ea6a2c7b7c with the new test in test/bake/dev/bundle.test.ts:

bun bd test test/bake/dev/bundle.test.ts -t "an export star merged"
# main: got: undefined function undefined function undefined A
# this branch: got: function function function function function A

The test passes on 1.4.2. A root import of @mui/material 7.3.11 in a Bun.serve HTML route shows the same result: useRadioGroup, ModalManager, ButtonGroupContext and stackClasses are undefined on 1.4.3 and defined on 1.4.2 and on this branch.

PR: #46229

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

Beyond the inline finding, I also checked the last_mut().unwrap() invariant against ImportScanner (src/js_parser/scan/scan_imports.rs:790 pushes the record for the unaliased export * only, immediately before the convert_stmt call at line 844, so the write lands on this statement's own entry) and the case of two export * statements of one path now listing one record twice — every reader of export_star_import_records already handles two records resolving to the same target, so that is not affected.

Extended reasoning...

The change is a four-line fix in the HMR ESM-export lowering pass plus a dev-server test; it touches no security-sensitive surface. The inline finding covers the sibling export * as ns arm, which routes through named_imports rather than export_star_import_records and is the same stale-record class the author lists as deferred to a follow-up PR; a human should weigh whether that split is acceptable.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/js_parser/lower/lower_esm_exports_hmr.rs — Dev-server users importing a namespace re-export (export * as ns from 'x') that follows another statement for 'x' in a sideEffects: false barrel get undefined for ns, while the bare export * one line below is fixed. The aliased arm at lower_esm_exports_hmr.rs:351 computes deduped but never writes the surviving record anywhere; named_imports (scan_imports.rs:778) still names the retired record. When ns is requested later, barrel_imports.rs:935 un-defers only the retired record and the surviving one stays deferred. Fix: write deduped.import_record_index into the named_imports entry for st.namespace_ref in the aliased arm too, so both SExportStar shapes name a live record. [also at: src/js_parser/lower/lower_esm_exports_hmr.rs:320 - Dev-server users of a sideEffects: false barrel still read undefined for a name re-exported by a second export { } from of a path already imported, after this merge, because only the export * sibling is repaired. lower_esm_exports_hmr.rs:320 rewrites symbol.namespace_alias to the surviving record, but p.named_imports (scan_imports.rs:813, :778) keeps the retired record.]

    Why this was flagged

    Trigger: a sideEffects: false package index with export { a } from './inner.js'; export * as ns from './inner.js';, served by the dev server, where neither a nor ns is requested when the barrel parses and ns is requested by a later-parsed module. deduplicated_import retires the second record (lower_esm_exports_hmr.rs:419-421). The aliased arm at :351-361 pushes a property using deduped.namespace_ref but, unlike the bare arm at :370-372, does not repair any record list. ImportScanner recorded named_imports[namespace_ref].import_record_index = st.import_record_index at scan_imports.rs:778, the retired one. When ns is requested, resolve_barrel_export (:899) yields the retired record, un_defer_record (:935) clears its flags and resolves it, but the surviving record that the HMR-lowered import * as statement references stays IS_UNUSED|IS_BARREL_DEFERRED, so inner.js is not loaded for that import and ns reads undefined. The base behaves the same, but the PR fixes the sibling arm of the same match and REVIEW.md treats same-class sites as one concern.

    Verification: pre-existing; acknowledged in diff: the PR description states "Not fixed: named_imports has the same stale record", and the aliased export * as ns arm is exactly a named_imports site (scan_imports.rs:771-782 records import_record_index: st.import_record_index for the aliased star). The base fails by the same route; the new test (test/bake/dev/bundle.test.ts) contains no export * as case.

@robobun

robobun commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

The export * as ns arm and a second export { } from are the named_imports half of the same defect. This PR leaves that half out on purpose.

To point named_imports at the surviving record changes what the barrel pass defers: the record of the merged statement becomes deferrable as one unit. A first version did that. The dev server then skipped a module that bun build runs (import {} from "p" before an unrequested export { a } from "p"). That half needs a way to tell the barrel pass what the merged statement needs, so it gets its own PR.

The export * list has no such effect. A record in that list is never deferred.

@dylan-conway dylan-conway changed the title js_parser: list the surviving import record of an export * that HMR lowering merges dev server: an export * after another statement of the same path loses exports of a barrel target Oct 21, 2026

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

I reviewed the updated version of this PR and did not find any bugs; since the fix sits in the dev-server barrel optimization path, which has had several ordering-sensitive regressions, a human look at the new commit would still be worthwhile.

What was reviewed:

  • Traced that under HMR every export * still reaches convert_stmt (no early exit before the SExportStar arm), so moving the push out of the scanner drops no records; the non-HMR scan path is unchanged and AstBuilder never fills the list.
  • Checked each reader of export_star_import_records (barrel_imports.rs, scanImportsAndExports.rs, bundle_v2.rs, js_printer, and the client staticExportNames walk) for the duplicate indices the star-twice fixture now produces — all are set/flag/idempotent operations.
  • Confirmed the latest commit removed the .last_mut().unwrap() rewrite from the prior version in favor of a plain push.
Extended reasoning...

The change is two production lines in the Rust js_parser (a push in the HMR lowering SExportStar arm and a const-generic gate on the scanner's push) plus one devTest with five fixtures in test/bake/dev/bundle.test.ts. It touches no security-sensitive surface. The logic is small and I traced it to every consumer, but it depends on the interplay of HMR import deduplication, the barrel deferral pass, and dev-server path resolution, an area with repeated prior regressions (#28886, #42737) and known remaining gaps the author lists, and the test could not be executed in this environment, so a human look is still warranted rather than an unattended approve.

dylan-conway commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

CI on 87492c11: buildkite/bun is red for a reason outside this PR.

  • Failing jobs: both :darwin: any aarch64 - test-bun jobs (1, 2), both on agent darwin-aarch64-26.6.1-1. Each stopped before any test ran:
    Error: buildkite-agent artifact download timed out after 120s for step 'darwin-aarch64-build-bun'.
  • The same agent failed with the same error on main, in build #130439.
  • Every other finished test job passed: alpine, debian, debian ASAN and ubuntu (x64 and aarch64), Windows x64 and aarch64. The two darwin x64 jobs are still waiting for an agent.
  • There is nothing to fix in the repository for this. The jobs need a retry in Buildkite, which I cannot trigger.

Generated by Claude Code

@dylan-conway
dylan-conway merged commit 9e4545f into main Oct 22, 2026
9 of 10 checks passed
@dylan-conway
dylan-conway deleted the robobun/c4617095/hmr-export-star-surviving-record branch October 22, 2026 00:06
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.

3 participants