Skip to content

js_parser: alias the server-components Response import only under hot reloading - #44167

Open
robobun wants to merge 4 commits into
robobun/6c050347/bake-response-outside-dev-serverfrom
robobun/6c050347/bake-response-import-binding
Open

robobun wants to merge 4 commits into
robobun/6c050347/bake-response-outside-dev-serverfrom
robobun/6c050347/bake-response-import-binding

Conversation

@robobun

@robobun robobun commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun build --server-components --target=bun and bun build --app write ES modules that read import_bun_app.Response and throw ReferenceError: import_bun_app is not defined. With --minify and two readers the load fails: SyntaxError: Cannot declare an imported binding name twice: 'Response'.
  • Cause: P::prepare_for_visit_pass (src/js_parser/p.rs:3508) gives the generated Response import item a namespace_alias in every output format. Only the dev server format declares that namespace.

Fix

  • Set the alias only under hot_module_reloading. Otherwise register the item in is_import_item before the visit. A debug_assert! in ImportScanner::scan checks the rule.
  • Correct because a live read now prints and links like a hand-written import { Response } from "bun:app".
  • Verified: test/bake/dev/response-to-bake-response.test.ts (11 of 17 tests fail without the fix), production.test.ts, 16 bundler suites.
  • Self-reviewed: 7 concerns raised, 6 addressed. Rejected: removal of the Co-authored-by trailer. The gate is a port of the hunk in SSG prod #23157.

Background

  • A namespace_alias makes the printer write namespace.name for a read. A symbol in is_import_item becomes an E::ImportIdentifier, which the linker and the renamers rebind.
  • Considered: clear the alias after the visit (keeps the store), a linker or printer patch (a cost on every import). The JSX import and esbuild use this gate.

Downsides

  • Built server code, node_modules included, now runs with the bun:app class. A fetch() result is not an instance of it. CommonJS output and the dev server already do.
  • A server-side file with no import item pays 2 heap allocations (104 B) for a 28 B arena box, and +17 instructions per identifier read.
  • Release .text: +0 B.
Notes

Stack

This PR is on top of #44166. Without that PR, built code that calls Response.redirect() or Response.render() reaches a crash in the bun:app class (Segmentation fault at address 0x0) where it had a ReferenceError before. The new run cases for redirect and render fail without it.

Reproduction

printf 'export default function Page() { return new Response("x"); }\nconsole.log(typeof Page());\n' > resp2.ts
bun build --server-components --target=bun ./resp2.ts --outfile out/resp2.js
bun out/resp2.js

Before: ReferenceError: import_bun_app is not defined, exit code 1. After: prints object.

bun build --app with the React framework and a page that runs new Response("x") exits 1 before the fix (ReferenceError: e is not defined at pages/index.tsx, e is the minified namespace name). After the fix it renders the page.

Reach

  • No user report exists. The output was found broken by a run of it.
  • The transform came with ssg 3 #22138. The ES module output is wrong in each release from bun-v1.3.0 to bun-v1.4.2. It never ran, so this is not a regression.
  • --app is refused on a build that is not a canary build.
  • The transform applies to each server-side file of the build. Files in node_modules are included (src/bundler/ParseTask.rs, the server_components mode has no is_node_module() check).

Why the alias was there

handle_identifier turns a read into an E::ImportIdentifier in two cases: the symbol has a namespace_alias, or the symbol is in is_import_item. The first version of the transform used the alias for this, for every format. jsx_import uses is_import_item for the same job, and generate_import_stmt adds the alias only under hot reloading. This change makes the Response import follow that rule. The two arms are exclusive: under hot reloading every read returns from the alias branch, so the map entry has no reader there.

The --format=cjs test proves the registration before the visit. Without it, the read stays an E::Identifier, the linker cannot rebind it, and the CommonJS output reads the global Response (the test then prints true true undefined). For CommonJS and IIFE output the linker adds its own alias and declares the namespace.

The debug_assert! sees only an alias that the parser put on an item of an import clause. It does not see a bare E::Identifier read of a generated symbol.

Output formats

Format Before After
esm (bun build, --splitting, --compile, --compile --bytecode --format=esm, bun build --app) throws or does not load runs
cjs, iife runs same bytes where the import is emitted
internal_bake_dev (dev server) runs same bytes

A read of Response in dead code that the printer keeps (if (false) { switch (new Response("x")) { case 1: } }) printed import_bun_app.Response with no import in esm, cjs and iife. It now prints Response.

Measurements

Debug builds for the counts (gdb breakpoints with counters). Release builds (--profile=release, one checkout path for both) for sizes and instruction text. The base is main 36cd151, the change is the parser commit alone on that base.

  • release .text: +0 B (llvm-size -A, 58,143,989 B in both); prepare_for_visit_pass +31 B (P<false, false>) and +32 B (P<true, false>, inlined in Parser::_parse::<true>), generate_import_stmt_for_bake_response -105 B (llvm-nm). No other function changes size. The sum of the function sizes is -42 B.
  • handle_identifier and ImportScanner::scan: 0 of 6 instantiations differ in size or instruction text (llvm-nm, llvm-objdump, main vs PR). The 6 are 2 of handle_identifier, 2 of ImportScanner::scan::<_, false, true>, and 2 of P::to_ast, which holds the inlined scan::<_, false, false>.
  • non-HMR server-side source that never reads Response (17 sources): alias boxes 17 -> 0; is_import_item inserts 0 -> 17, of which 16 grew an empty map (2 heap allocations each, 8 B + 96 B); JSON sources among them: 5 (gdb breakpoint hits, debug build, main vs PR).
  • identifier reads that now probe a one-entry is_import_item: 1,562 reads x +17 instructions = +26,554 instructions on the 17-source fixture (gdb count x llvm-objdump path count: 11 instructions for the empty map, 28 for a miss in a one-entry map). callgrind Ir delta: not measured. valgrind and perf are not installed, and apt cannot reach its sources in this environment.
  • without --server-components: 0 hits on the new insert line, 0 on the alias-box line, bundle and runtime transpiler (gdb, main vs PR).
  • internal_bake_dev output: 0 differing bytes in 5 files (cmp, main vs PR); Response is_import_item inserts under hot reloading: 2 -> 1 for a fixture with two readers (gdb). The 1 is the bundler runtime module, which is parsed with hot reloading off. react-response.test.ts: 11 pass, 0 fail.
  • cjs and iife output of the Response fixtures: 0 differing bytes in 16 of 20 files (cmp, main vs PR). The 4 files that differ are the dead-code fixture, where import_bun_app.Response becomes Response (43 to 60 bytes each).

Suites run on the debug build

  • test/bake/dev/response-to-bake-response.test.ts: 17 pass. The same file with BUN_JSC_validateExceptionChecks=1 and LeakSanitizer, as the ASAN lane runs it: 17 pass.
  • test/bake/dev/production.test.ts, test/bake/dev/react-response.test.ts, test/bake/dev/bundle.test.ts, test/bake/app-options.test.ts: 49 pass, 0 fail (--timeout 180000).
  • 16 bundler suites (esbuild/default, importstar, importstar_ts, ts, dce, splitting, extra, bundler_edgecase, bundler_jsx, bundler_cjs, bundler_cjs2esm, bundler_minify, bundler_splitting, bundler_dynamic_import_dce, bundler_npm, cli): 1681 pass, 0 fail. The new assertion did not fire.

Test placement

The bun build --app test is in production.test.ts. That file is in test/no-validate-exceptions.txt: bun build --app stops under BUN_JSC_validateExceptionChecks=1 with or without this change (#41185). On a debug build each test of that file needs --timeout, because a production build takes more than 5 s there.

Self-review

Concern Result
Built code that calls Response.redirect() crashes where it threw before This PR is now on top of #44166, the runtime guard. Run cases for redirect and render added.
The --app test aborts on the ASAN lane in a file with exception validation Moved to production.test.ts.
The timeout argument lowers the budget on the ASAN lane Removed.
The description does not say what the bun:app class does in built output Added to Downsides and to the list below.
"Same as a hand-written import" is too strong The sentence now covers live reads only. The differences are in the list below.
"CommonJS, IIFE and dev server output do not change" is false for dead code Qualified above.
Remove the Co-authored-by trailer Rejected. The gate is a port of the hot_module_reloading hunk of #23157 (src/ast/P.zig).

The review on this PR had three more comments. The assertions now include the token before each read, so each one fails on the old output. A run case for --format=iife is added. A --compile case is not added: the compile step takes 6 to 13 s on a debug build, which is more than the default timeout of a local test run.

Not changed here


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bake/dev/production.test.ts

…item outside hot reloading

The parser rewrites the global `Response` of a server-side file to an
import from "bun:app". It attached a namespace alias to that import item
for every output format. Only the dev server module format declares the
namespace object. In ES module output each use site printed
`import_bun_app.Response`, and nothing declared `import_bun_app`.

Attach the alias only for hot reloading. In every other format, register
the item in `is_import_item` before the visit, as the JSX runtime import
does. The linker adds its own alias when it converts the import for
CommonJS or IIFE output.

A debug assertion in the import scanner checks that no import clause
item has an alias when the bundler links.

Co-authored-by: Zack Radisic <56137411+zackradisic@users.noreply.github.com>
@robobun

robobun commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. This PR is on top of #44166.

Reproduced on 1.4.3-canary.1+367d939d9 (release) and on a debug build of main:

printf 'export default function Page() { return new Response("x"); }\nconsole.log(typeof Page());\n' > resp2.ts
bun build --server-components --target=bun ./resp2.ts --outfile out/resp2.js
bun out/resp2.js
  • Before: the output has import { Response } from "bun:app"; and new import_bun_app.Response("x"). It throws ReferenceError: import_bun_app is not defined, exit code 1.
  • With --minify and two files that read Response: SyntaxError: Cannot declare an imported binding name twice: 'Response'.
  • bun build --app with a page that runs new Response("x"): exit code 1, ReferenceError: e is not defined.
  • With this change the output prints object, and bun build --app renders the page.

@robobun
robobun requested a review from alii September 28, 2026 19:36
@robobun

robobun commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

@robobun wake up!!

@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 findings, I also checked the new debug_assert! in src/js_parser/scan/scan_imports.rs against every other parser site that attaches a namespace_alias to an import clause item (p.rs lines 2559, 4408, 4489 and the scanner's own import-star path at scan_imports.rs:548) — each is either gated on hot_module_reloading or runs after the assertion on the same pass, so the assertion should not fire on ordinary ESM/CJS bundles.

Extended reasoning...

The change gates the parser-attached bun:app namespace alias on hot reloading and registers the generated Response symbol in is_import_item otherwise, plus a debug-only assertion and test updates; it touches no security-sensitive surface. Inline findings already cover the behavioral concerns, so this note only records that the other alias-setting sites were checked for the new assertion.

Comment thread test/bake/dev/response-to-bake-response.test.ts Outdated
Comment thread test/bake/dev/response-to-bake-response.test.ts
Comment thread src/js_parser/p.rs
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@robobun wake up!!

@robobun

robobun commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

I am here. Build #121331 passed. The review has three optional comments. I will push a test-only commit for two of them (assertions that cannot fail, a run case for iife) and answer in the threads. The third one (the transform also applies to files in node_modules) is existing behaviour and is under Downsides in the description.

Four assertions looked for text that the fixture source has too, for
example `Response.json(`. The old output `import_bun_app.Response.json(`
contains that text, so they passed with or without the transform. Each
assertion now includes the token before the read.

Add a run case for `--format=iife`. The linker converts the import with
`__require` there.
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/p.rs Outdated
Comment thread src/js_parser/scan/scan_imports.rs Outdated
@robobun

robobun commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:03 PM PT - Sep 28th, 2026

✅ @robobun, your commit d118a9780cf4ec2cf8c8f15c9f40071a416ce592 passed in Build #121477! 🎉


🧪   To try this PR locally:

bunx bun-pr 44167

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

bun-44167 --bun

@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 re-reviewed after b3c4049 and found no bugs; the follow-up anchors the flipped assertions to the surrounding source text and adds the --format=iife run case, which addresses the two test nits from the earlier pass. Because this changes how the parser registers a generated import in the server-components path and a maintainer is already in the thread, a human look is still worthwhile.

What was reviewed:

  • The removed post-visit is_import_item.insert(self.response_ref, ()) in generate_import_stmt_for_bake_response: the only is_import_item readers that run after that point (dynamic-import destructure locals in parse_entry.rs and import_items_for_namespace) never see response_ref, so nothing under hot reloading loses the entry.
  • The new debug_assert! in ImportScanner::scan: every other parser site that attaches a namespace_alias to a clause item (jsx/generated imports, default and named clause items) is already gated on hot_module_reloading, and the WAS_ORIGINALLY_REQUIRE site aliases the namespace ref, not an item, so the assertion cannot fire on existing paths.
  • features.hot_module_reloading is set only for InternalBakeDev non-runtime sources (ParseTask.rs), matching the const generic the scan is instantiated with.
Extended reasoning...

The change gates the parser-attached namespace_alias on the server-components Response import to hot-reloading mode and otherwise registers the symbol in is_import_item before the visit, plus a debug assertion in the import scanner and test updates that run built esm/cjs/iife/minified/split output. It touches the JS parser and bundler output shape for bun build --server-components and --app; no security-sensitive surface. The source change is small and traced through its consumers, and the latest commit resolved the two test-quality nits from the prior review, but it alters codegen in a core parser path and a maintainer comment whose content is not visible to this run precedes the last push, so approval is left to a human.

Keep the first field doc as it was. State the namespace ref in two
lines and the assertion in one.
Comment thread src/js_parser/p.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.

Re-reviewed after the two follow-up commits and found no bugs; the earlier inline nits are addressed in the code (anchored assertions plus not.toContain("import_bun_app"), and an iife run case). Since this changes how the parser classifies a generated import symbol, a maintainer look at the parser hunk is still worthwhile.

What was reviewed:

  • The HMR-only namespace_alias.import_record_index patch in generate_import_stmt_for_bake_response was already gated on hot_module_reloading before this PR, so the now-absent alias in non-HMR mode is never unwrapped.
  • The scanner debug_assert! is only live in the scan::<_, _, false> instantiation, which is the branch taken exactly when hot_module_reloading is off, matching the parser gate.
  • The synthetic import is emitted only when response_ref.use_count_estimate > 0, so the non-HMR import scanner cannot elide it as unused.
  • The new production.test.ts case follows the sibling test's build/read pattern in the same file.
Extended reasoning...

The diff touches src/js_parser/p.rs (parser init and the bun:app import synthesis), a debug assertion in src/js_parser/scan/scan_imports.rs, and two bake test files; it changes how the generated Response symbol is registered (namespace alias only under HMR, otherwise a plain import item) and no security-sensitive surface is involved. The third review pass found no new bugs and the follow-up commits address the previously posted nits. The change is small but alters symbol classification in the JS parser used by every server-components build, and a maintainer comment whose content is not visible here exists on the thread, so a human look is warranted rather than an automated approval.

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

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