Skip to content

js_printer: fix require("bun") and other printer literals being captured by same-named locals - #35739

Open
robobun wants to merge 10 commits into
mainfrom
farm/0c9c6d19/fix-require-bun-shadowed-globalthis
Open

js_printer: fix require("bun") and other printer literals being captured by same-named locals#35739
robobun wants to merge 10 commits into
mainfrom
farm/0c9c6d19/fix-require-bun-shadowed-globalthis

Conversation

@robobun

@robobun robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes #8058.

What does this PR do?

The printer rewrites require("bun") / import("bun") / import ... from "bun" to the literal text globalThis.Bun (and Promise.resolve(globalThis.Bun) for the dynamic case). A user binding named globalThis in scope at the call site shadows that literal:

{ let globalThis = { Bun: "x" }; console.log(require("bun")) }
// prints: x   (should be the Bun object)

print_require_or_import_expr and print_global_bun_import_statement emit globalThis.Bun as raw text with no Ref, so it never participates in renaming. In the bundler a renamer runs, but globalThis was not in its reserved-name seed, so a user's let globalThis was left in place. In the runtime transpiler that path uses NoOpRenamer, so reserving the name would not help there.

Two changes, one for each path:

  • Runtime: the globalThis.Bun literal is no longer emitted. require("bun") and import("bun") fall through to the normal external paths (the module loader resolves "bun" to the Bun object, so require("bun") === Bun still holds). Static import ... from "bun" keeps its var-destructure lowering but now sources from import.meta.require("bun"), which is shadow-proof (import.meta is syntax) and keeps the lazy property access that the destructure provides; emitting a real ESM import instead would eagerly reify every Bun property and reject type-only names such as ShellError at link time.
  • Bundler: add globalThis to compute_initial_reserved_names so the NumberRenamer renames a user's let globalThis away before globalThis.Bun is emitted. bundler: seed reserved names with globalThis/Error/Infinity/NaN so locals can't capture printer literals #35575 made the same change and also reserved Error, Infinity, NaN and undefined, which the printer emits as raw text in the same way (inlined require errors, number printing, synthesized undefined); that PR is now folded in here and closed, so the seed reserves all of them.

The transpiler cache version is bumped so cached entries containing the old globalThis.Bun text are invalidated.

await import("bun") now returns the module namespace object (ns.default === Bun, every Bun.* property re-exported) instead of Bun itself. This matches what await import(spec) already returned for a non-literal spec. require("bun"), import Bun from "bun", and import * as B from "bun" still yield Bun directly as before. The bundler still inlines globalThis.Bun for the dynamic-import case (its output is unchanged); aligning that with the runtime namespace shape is left for a follow-up. (#37730 does that and touches the same print_require_or_import_expr block and cache version; whichever of the two lands second needs a small rebase.)

One more observable side effect of the runtime lowering: a literal require("bun") or import ... from "bun" now actually evaluates a require("bun"), so "bun" appears in require.cache (previously only a non-literal require(spec) with spec === "bun" did that; the printer rewrite never touched the loader). test/cli/inspect/BunFrontendDevServer.test.ts's module-graph snapshot is updated for this, since its server.ts fixture imports serve from "bun".

How did you verify your code works?

New tests in test/js/bun/resolve/import-meta.test.js cover require("bun"), dynamic import("bun"), static import * as B from "bun", and the CommonJS form, each with a shadowing let globalThis (and let Promise for the dynamic case). A guard test asserts that import { env } from "bun" still does not eagerly reify the whole Bun object. New itBundled cases in test/bundler/bundler_bun.test.ts cover the ESM and CJS bundler output with shadowed globalThis for all three forms. The itBundled cases from #35575 are carried over as well: shadowed globalThis with and without --minify-identifiers, plus shadowed Error (inlined require error), Infinity (1e400), NaN (folded 0/0) and undefined (synthesized import.meta.hot).

On current main, 14 of the tests across the two files fail (the 3 Minified variants and the reification guard pass on both sides and only guard the behavior); all 59 pass with this branch. The transpiler cache version is 26 after merging main, which took 24 and 25 in the meantime. BunObject.test.ts, the bun:shell suite, and the existing import-meta/bundler_bun/bundler_minify tests continue to pass.


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

fails on main (without fix)
ASAN without fix: 7 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/bundler_bun.test.ts test/js/bun/resolve/import-meta.test.js
bun test v1.4.0 (f5cb2b2df)

test/bundler/bundler_bun.test.ts:
21 |         }
22 |       `,
23 |     },
24 |     run: { stdout: "pass" },
25 |     onAfterBundle(api) {
26 |       expect(api.readFile("out.js")).not.toContain(`globalThis = { Bun`);
                                              ^
error: expect(received).not.toContain(expected)

Expected to not contain: "globalThis = { Bun"
Received: "// @bun\nvar __require = import.meta.require;\n\n// entry.ts\nvar B =globalThis.Bun;\nif (typeof B.serve !== \"function\")\n  throw new Error(\"import * from 'bun' was shadowed: \" + B);\n{\n  let globalThis = { Bun: \"intercepted\" };\n  const b = globalThis.Bun;\n  if (typeof b.serve !== \"function\")\n    throw new Error(\"require('bun') was shadowed: \" + b);\n  const d = await Promise.resolve(globalThis.Bun);\n  if (typeof d.serve !== \"function\")\n    throw new Error(\"import('bun') was shadowed: \" + d);\n  console.log(\"pass\");\n}\n"

      at onAfterBundle (/w
... (truncated)

release without fix: 7 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/bundler/bundler_bun.test.ts:
21 |         }
22 |       `,
23 |     },
24 |     run: { stdout: "pass" },
25 |     onAfterBundle(api) {
26 |       expect(api.readFile("out.js")).not.toContain(`globalThis = { Bun`);
                                              ^
error: expect(received).not.toContain(expected)

Expected to not contain: "globalThis = { Bun"<r>
Received: <red>"// @bun\nvar __require = import.meta.require;\n\n// entry.ts\nvar B =globalThis.Bun;\nif (typeof B.serve !== \"function\")\n  throw new Error(\"import * from 'bun' was shadowed: \" + B);\n{\n  let globalThis = { Bun: \"intercepted\" };\n  const b = globalThis.Bun;\n  if (typeof b.serve !== \"function\")\n    throw new Error(\"require('bun') was shadowed: \" + b);\n  const d = await Promise.resolve(globalThis.Bun);\n  if (typeof d.serve !== \"function\")\n    throw new Error(\"import('bun') was shadowed: \" + d);\n  console.log(\"pass\");\n}\n"

      at onAfterBundle (/workspace/bun/test/bundler/bundler_bun.test.ts:26:42)
      at <anonymous> (/workspace/bun/test/bundler/expectBundled.ts:1591:7)
(fail) bundler > bun/require-bun-shadowed-globalThis [56.81ms
... (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_bun.test.ts test/js/bun/resolve/import-meta.test.js
bun test v1.4.0 (f5cb2b2df)

test/bundler/bundler_bun.test.ts:
(pass) bundler > bun/require-bun-shadowed-globalThis [1557.06ms]
(pass) bundler > bun/require-bun-shadowed-globalThis-cjs [751.23ms]
(pass) bundler > bun/import-bun-format-cjs [906.18ms]
(pass) bundler > bun/embedded-sqlite-file [819.38ms]
(pass) bundler > bun/sqlite-file [689.15ms]
(pass) bundler > bun/TargetBunNoSourcemapMessage [1069.58ms]
(pass) bundler > bun/TargetBunSourcemapInline [1226.56ms]
(pass) bundler > bun/unicode comment [536.62ms]
(pass) bundler > bun/ExportsConditionsDevelopmentAPI [1192.32ms]
(pass) bundler > bun/ExportsConditionsDevelopmentInProductionAPI [584.26ms]
(pass) bundler > bun/ExportsConditionsDevelopmentCLI [1114.32ms]
(pass) bundler > bun/ExportsConditionsDevelopmentInProductionCLI [1239.05ms]

test/js/bun/resolve/import-meta.test.js:
(pass) import.meta.require is settable [16.71ms]
(pass) import.meta.main [404.95ms]
(pass) import.meta.resolveSync [2.93ms]
(pass) Module.c
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     f5cb2b2dfd
  features     baseline

22 deps, 108 codegen, 1171 objects in 4350ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1234] gen ErrorCode+*.h
[2/1234] gen bindgenv2
[3/1234] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[4/1234] fetch tinycc
[tinycc] up to date
[5/1234] fetch zlib
[zlib] up to date
[6/1234] fetch picohttpparser
[picohttpparser] up to date
[7/1234] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[8/1234] gen ProcessBindingConstants.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingConstants.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingConstants.cpp
[9/1234] gen ProcessBindingFs.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingFs.lut.h from /workspace/bun/src/jsc/bindings/ProcessBindingFs.cpp
[10/1234] gen ProcessBindingHTTPParser.lut.h
Generating /workspace/bun/build/release/codegen/ProcessBindingHTTPParser.lut.h fr
... (truncated)
diff hotspot
src/js_printer/lib.rs                   | 17 ++++++-
 src/js_printer/renamer.rs               |  3 +-
 src/jsc/RuntimeTranspilerCache.rs       |  4 +-
 test/bundler/bundler_bun.test.ts        | 40 +++++++++++++++
 test/js/bun/resolve/import-meta.test.js | 88 +++++++++++++++++++++++++++++++--
 5 files changed, 145 insertions(+), 7 deletions(-)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                                     reads  edits  tests
src/js_printer/lib.rs                       19     11      0
src/js_printer/renamer.rs                    4      3      0
src/jsc/RuntimeTranspilerCache.rs            4      2      0
test/bundler/bundler_bun.test.ts             1      1      0
test/js/bun/resolve/import-meta.test.js      3      5      0

A local `let globalThis` would shadow the literal `globalThis.Bun` that
the printer emitted for require('bun') / import('bun') / import ... from 'bun'.

Runtime path: the single-file transpiler uses NoOpRenamer, so no renaming
can protect the literal. Gate the inline on `options.bundling` and let the
import fall through to require('bun') / import('bun'), which the module
loader resolves to the Bun object.

Bundler path: reserve `globalThis` in compute_initial_reserved_names so the
NumberRenamer renames a user local away before the literal is emitted.

Fixes #8058
@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 8:05 AM PT - Aug 13th, 2026

@robobun, your commit c96c43c has some failures in Build #94441 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 35739

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

bun-35739 --bun

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Bun import and require rewrites now apply only during bundling, globalThis is reserved during renaming, the runtime transpiler cache version is updated, and regression tests cover shadowed globals across runtime transpilation and bundling.

Changes

Bun import resolution

Layer / File(s) Summary
Printer guards and compatibility updates
src/js_printer/lib.rs, src/js_printer/renamer.rs, src/jsc/RuntimeTranspilerCache.rs
Bun-specific rewrites require bundling, globalThis is reserved during renaming, and the runtime transpiler cache version advances to invalidate older entries.
Import resolution regression coverage
test/js/bun/resolve/import-meta.test.js, test/regression/issue/08058.test.ts
Tests verify Bun namespace imports and runtime or bundled imports remain correct when local globalThis or Promise bindings are present.

Possibly related PRs

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses #8058 by reserving globalThis in bundling and preventing shadowable globalThis.Bun output.
Out of Scope Changes check ✅ Passed The extra runtime, cache, and test changes all support the stated fix and are in scope.
Title check ✅ Passed The title clearly identifies the printer fix for shadowed locals affecting require("bun") and related literals.
Description check ✅ Passed The description includes both required sections and provides detailed implementation context, behavior changes, and verification results.

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

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

🤖 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 `@test/regression/issue/08058.test.ts`:
- Around line 2-6: Remove the explanatory header comment at
test/regression/issue/08058.test.ts lines 2-6, retaining only the issue URL on
line 1; also remove the explanatory comment at lines 102-102 so the regression
test contains exactly the issue URL comment.
- Line 10: Move the `itBundled` test cases currently in
`test/regression/issue/08058.test.ts` (lines 85–122) into a test file under
`test/bundler/`, preserving their assertions and required imports so `itBundled`
accepts the callers and runs the bundled tests.
- Around line 85-86: Add bundled execution coverage in the `bundler` tests for
the changed `SImport` path: add `import * as B from "bun"` cases where
`globalThis` is shadowed, and run each case in both ESM and CJS output modes.
Follow the existing `itBundled` patterns in
`test/regression/issue/08058.test.ts` and retain assertions verifying successful
bundled 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: 09a8b673-27a5-4225-b57a-ca2037dea9f2

📥 Commits

Reviewing files that changed from the base of the PR and between df6c7ee and 290bae6.

📒 Files selected for processing (5)
  • src/js_printer/lib.rs
  • src/js_printer/renamer.rs
  • src/jsc/RuntimeTranspilerCache.rs
  • test/js/bun/resolve/import-meta.test.js
  • test/regression/issue/08058.test.ts

Comment thread test/regression/issue/08058.test.ts Outdated
Comment thread test/regression/issue/08058.test.ts Outdated
Comment thread test/regression/issue/08058.test.ts Outdated
@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. bundler: seed reserved names with globalThis/Error/Infinity/NaN so locals can't capture printer literals #35575 - Both fix subtle bundler bug, require("bun") in a scope that defines local variable named globalThis will bundle incorrectly #8058 by adding globalThis to compute_initial_reserved_names in the bundler renamer; bundler: seed reserved names with globalThis/Error/Infinity/NaN so locals can't capture printer literals #35575 is a strict subset (bundler-only fix) of this PR's broader runtime+bundler fix

🤖 Generated with Claude Code

Comment thread src/js_printer/lib.rs
…at runtime

Emitting a real ESM import for the statement form (a) eagerly reifies every
property on the Bun object via generateNativeModule_BunObject, and (b) fails
link-time validation for names that exist only as types in bun.d.ts (e.g.
ShellError). Limit the runtime change to require('bun') / import('bun').
Comment thread src/js_printer/lib.rs Outdated
Comment thread src/js_printer/lib.rs Outdated
Comment thread src/js_printer/renamer.rs Outdated
Comment thread src/jsc/RuntimeTranspilerCache.rs Outdated
Comment thread src/js_printer/lib.rs
Comment thread src/js_printer/lib.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.

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/js_printer/lib.rs:5772-5779 — The PR description lists the runtime static-import TDZ case (import * as B from "bun"; let globalThis = {}) as one of the two #8058 failure modes being fixed, but commit 7ed11a5 deliberately kept this lowering ungated at runtime — so that case still throws ReferenceError: Cannot access 'globalThis' before initialization, and the runtime describe() block has no test for it. Since the exclusion is intentional (reification / link-time-validation, per the new comment), please update the description to note it and either drop Fixes #8058 or add a note on the issue about the remaining limitation.

    Extended reasoning...

    What's mismatched

    The PR description's Problem section lists two manifestations of #8058:

    1. { let globalThis = { Bun: "x" }; console.log(require("bun")) } → prints x
    2. import * as B from "bun"; let globalThis = {};ReferenceError: Cannot access 'globalThis' before initialization (TDZ)

    The Fix section says the runtime path now lets import ... from "bun" fall through to the normal external path, and the Tests section says all six cases (require / dynamic import / static import × runtime / bundler) fail on main and pass with this change. But the shipped code fixes case (1) at runtime and case (2) only in the bundler — case (2) at runtime is deliberately left as-is.

    The code path

    • print_require_or_import_expr at lib.rs:2435 is now gated on self.options.bundling, so runtime require("bun") / import("bun") fall through to the real module loader. ✅
    • The SImport handler at lib.rs:5772 is not gated on bundling — the new comment explicitly says so ("Unlike print_require_or_import_expr this is not gated on bundling"), and commit 7ed11a5 ("keep the var-destructure lowering for static import ... from 'bun' at runtime") reverted an earlier attempt to gate it.
    • print_global_bun_import_statement (lib.rs:1718–1722) calls print_internal_bun_import(import, Some(b"globalThis.Bun")), which for a star import emits var B = globalThis.Bun; (lib.rs:1734–1751).
    • The runtime transpile path uses NoOpRenamer (renamer.rs — "only constructed by print_ast/print_common_js"), so no user binding is renamed. The new compute_initial_reserved_names entry for globalThis only helps the bundler's NumberRenamer.

    The new comment at :5772 justifies keeping the lowering ("eagerly reify every property on the Bun object" / "fail link-time validation for names that exist only as types") and says "When bundling, the renamer has already renamed any user globalThis away" — but is silent on what happens when not bundling, which is exactly where the TDZ case lives.

    Step-by-step proof

    Given entry.ts:

    import * as B from "bun";
    let globalThis = {};
    console.log(typeof B.serve);
    1. Runtime transpiler hits Stmt::ImportSImport arm at lib.rs:5772. IS_BUN_PLATFORM is true, record.tag == ImportRecordTag::Bun is true, no options.bundling gate → print_global_bun_import_statement(s).
    2. print_internal_bun_import sees !import.star_name_loc.is_empty() → emits var B = globalThis.Bun;.
    3. let globalThis = {} prints unchanged (NoOpRenamer::name_for_symbol returns original_name).
    4. Emitted module: var B = globalThis.Bun; let globalThis = {}; console.log(typeof B.serve);
    5. At execution, the module-scope let globalThis is hoisted into TDZ; line 1's globalThis.Bun reads it before initialization → ReferenceError: Cannot access 'globalThis' before initialization.

    This is byte-identical to the pre-PR behavior for this specific input.

    Test coverage gap

    test/regression/issue/08058.test.ts's describe("runtime transpiler") block has four tests: require('bun') shadowed, dynamic import('bun') shadowed, .cjs require shadowed, and a reification assertion (which asserts the lowering is kept, not that shadowing works). There is no runtime test for import * as B from "bun" with a module-level let globalThis. The bundler describe() covers it via the reserved-name fix, but that path uses NumberRenamer. So the description's "All six cases fail on main and pass with this change" over-counts — the runtime static-import case is neither fixed nor tested.

    Why this is a nit

    • No regression: the runtime static-import output for this input is unchanged from before the PR; nothing that worked now breaks.
    • Intentional and documented in code: commit 7ed11a5 explicitly reverted the gate, and the code comment states the trade-off (eager reification of the whole Bun object; link-time validation failure for type-only names in bun.d.ts). Those are real concerns.
    • Pathological input: a module-top-level let globalThis co-located with import ... from "bun" is vanishingly rare.

    Per REVIEW.md ("Fix the whole class in the same PR … If a site is intentionally excluded, say so in the PR"), the ask is just to make the exclusion visible: update the PR description so it doesn't claim the runtime static-import TDZ case is fixed, and either drop Fixes #8058 or leave a note on the issue about the remaining limitation. Optionally, add a runtime test that documents the current behavior so the gap is explicit rather than silent.

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Re the runtime static-import TDZ note: the PR body was updated before that comment landed. The Problem section no longer lists the TDZ case, the Fix section explicitly says the static import ... from "bun" lowering in the runtime transpiler is left as-is (with the reification / link-time-validation rationale), and the Tests section accurately describes the six tests that ship. #8058's original repro is bun build --target=bun with require("bun") in a block that declares let globalThis; that's the bundler case and is fixed here, so Fixes #8058 holds.

…me; move tests to module files

Runtime static imports now destructure from `import.meta.require("bun")`
instead of the bare `globalThis.Bun` literal, closing the module-scope
`let globalThis` shadow case without going through a real ESM import
(which would eagerly reify every Bun property and reject type-only names).

Tests moved out of test/regression/issue/: this was never a regression
(the rewrite has had this bug since it was introduced). Runtime cases go
in import-meta.test.js next to the existing require('bun') tests; bundler
cases go in bundler_bun.test.ts.
Comment thread src/js_printer/lib.rs
Comment thread test/js/bun/resolve/import-meta.test.js Outdated
@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the diff itself is green where it ran (Lint JavaScript and Source lints pass; local bun bd test passes on import-meta.test.js, bundler_bun.test.ts, BunObject.test.ts, the bun:shell suite, and transpiler-cache.test.ts). The Buildkite failures on builds 81166, 81360, and 81481 are all build-infrastructure: the Rust lane finishes in ~1m50s and then times out waiting on *-build-cpp, or the jobs expire in the queue before an agent picks them up. None of the failures are test failures, and the same pattern is showing on main builds in the same window. This is ready for review once CI infrastructure recovers; happy to rebase/retrigger at that point.

…uire-bun-shadowed-globalthis

Transpiler cache version bumped to 26: main took 24 and 25 in the meantime.
…ls (from #35575)

Folds in #35575: those identifiers are printed as raw text too (inlined
require errors, number printing, synthesized undefined), so a local with
one of these names captured them the same way a local globalThis captured
the require("bun") rewrite. Carries over that PR's bundler tests.
Comment thread src/js_printer/renamer.rs
Comment thread src/jsc/RuntimeTranspilerCache.rs
@robobun robobun changed the title js_printer: don't rewrite require('bun') to globalThis.Bun at runtime js_printer: fix require("bun") and other printer literals being captured by same-named locals Aug 13, 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 this PR (including the folded-in #35575 reserved-name additions and the post-merge cache-version bump to 26) and didn't find any bugs. Because it changes runtime transpiler output for require('bun')/import('bun') and shifts the observable shape of await import('bun'), a maintainer sign-off would still be worthwhile.

What was reviewed:

  • The options.bundling gate in print_require_or_import_expr and the import.meta.require("bun") lowering in print_global_bun_import_statement; the contains_import_meta flag write matches the existing pattern at the other three sites.
  • The five new EXTRAS reserved names — ensure_total_capacity already accounts for EXTRAS.len(), so no capacity under-reservation.
  • Cache version 26 is unique after the main merge (24/25 were taken).
  • The two open comment-cop flags on renamer.rs/RuntimeTranspilerCache.rs are re-fires of content the author already justified on 07-25.
Extended reasoning...

Overview

The PR stops the runtime transpiler from rewriting require('bun') / import('bun') to the raw literal globalThis.Bun (which a user let globalThis could shadow — #8058). The runtime now falls through to the real module loader for the expression forms, and lowers static import ... from 'bun' via import.meta.require('bun'). On the bundler side (which still emits globalThis.Bun), globalThis plus Error/Infinity/NaN/undefined (folded in from #35575) are added to compute_initial_reserved_names so user locals with those names get renamed away. The transpiler cache version is bumped to 26. Tests cover the runtime forms (import-meta.test.js, describe.concurrent) and the bundler forms (bundler_bun.test.ts, both minify variants and each new reserved name).

Security risks

None identified. This is a code-generation correctness fix in the printer/renamer; no auth, crypto, filesystem, or untrusted-input parsing is touched.

Level of scrutiny

High. src/js_printer/lib.rs is on the critical path for every transpiled module, and the change alters user-observable behavior: await import('bun') now returns the module namespace object (ns.default === Bun) rather than Bun itself. The PR body documents this and the resulting runtime↔bundler divergence (bundler output is byte-identical to before), with #37730 tracking the bundler-side alignment. That is a design decision a maintainer should confirm rather than an automated approval.

Other factors

  • My two prior rounds of feedback (namespace-shape divergence note; describe.concurrent for the subprocess suite) were both addressed.
  • Since my last look, #35575 was folded in and main was merged; I checked the merged reserved-name list, the corresponding itBundled cases for Error/Infinity/NaN/undefined, and that the capacity reservation in compute_initial_reserved_names still uses EXTRAS.len() so it stays correct at 7 entries.
  • The contains_import_meta = true write in the new runtime branch mirrors the three existing sites in the same file.
  • The updated 'import("bun") works' test now asserts ns.default === Bun and consistency with import(eval("'bun'")), locking in the new namespace shape.
  • The two unresolved comment-cop bot comments dated today are re-fires on the same two-line comment / changelog entry the author already justified on 2026-07-25; nothing new to act on there.

server.ts does `import { serve } from "bun"`, which the runtime now lowers
to a require("bun"); that registers "bun" in require.cache the same way a
non-literal require("bun") always has.

@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 the Aug 13 push (folding in #35575's reserved names, the builtin-esm-lazy-exports comment updates, and the BunFrontendDevServer module-graph snapshot) and found no bugs; my earlier notes on the namespace-shape divergence and describe.concurrent are addressed. A human sign-off is still worthwhile for the user-visible runtime change: await import("bun") now yields the namespace object (ns.default === Bun, ns !== Bun) instead of Bun itself, and "bun" now appears in require.cache.

Checked: the contains_import_meta flag write matches the three existing call sites; import.meta.require("bun") is only reached on the ESM SImport path so import.meta is always valid; the cache version bump to 26 is contiguous after the merge; the extended EXTRAS list is covered by the new itBundled cases for Error/Infinity/NaN/undefined.

Extended reasoning...

Overview

The PR fixes #8058 by (a) gating the printer's globalThis.Bun rewrite on options.bundling so the runtime transpiler falls through to real require/import for "bun", with the static import ... from "bun" form now sourcing from import.meta.require("bun"); and (b) seeding the bundler's reserved-name set with globalThis, Error, Infinity, NaN, undefined so user locals with those names get renamed away before the printer emits them as raw text. The transpiler cache version is bumped to 26. Tests cover both runtime and bundler paths across require/dynamic-import/static-import × ESM/CJS × minified/unminified, plus the four folded-in printer literals from #35575. Two existing test files are updated for the observable side effects ("bun" in the inspector module graph's cjs list; the import("bun") shape assertion).

Security risks

None. No untrusted input handling, auth, or network surface changes.

Level of scrutiny

Moderate-to-high. The printer and renamer are critical hot paths, and the runtime-side change alters user-observable semantics: await import("bun") now returns the module namespace object rather than Bun directly, and a literal require("bun") / import ... from "bun" now populates require.cache["bun"]. The PR body documents both, the bundler output is byte-identical to before, and the runtime side is now consistent with the non-literal-specifier path — but code that did (await import("bun")) === Bun or relied on .default being absent will observe the difference. That's a maintainer-level call, not something an automated review should sign off on alone.

Other factors

My two prior inline notes (the runtime↔bundler namespace-shape divergence, and describe.concurrent for the subprocess suite) were both addressed. The comment-cop bot's flags on this push were dismissed with valid rationale (changelog entry format; two-line invariant doc). The contains_import_meta = true write follows the exact pattern of the three existing sites at lib.rs:2990/3020/7516. Test coverage is thorough, including a guard test that import { env } from "bun" still doesn't eagerly reify the whole Bun object. The PR description also flags an expected rebase interaction with #37730 on the same block.

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.

subtle bundler bug, require("bun") in a scope that defines local variable named globalThis will bundle incorrectly

2 participants