From 290bae6af5061b76cf82b2e58333dd2acdb56685 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:17:38 +0000 Subject: [PATCH 1/9] js_printer: don't rewrite require('bun') to globalThis.Bun at runtime 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 --- src/js_printer/lib.rs | 15 ++- src/js_printer/renamer.rs | 6 +- src/jsc/RuntimeTranspilerCache.rs | 4 +- test/js/bun/resolve/import-meta.test.js | 6 +- test/regression/issue/08058.test.ts | 122 ++++++++++++++++++++++++ 5 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 test/regression/issue/08058.test.ts diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index a0c4584f3afe..ea1b03e5d2f4 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -2432,7 +2432,7 @@ pub mod __gated_printer { let record = self.import_record(import_record_index as usize); let module_type = self.options.module_type; - if IS_BUN_PLATFORM { + if IS_BUN_PLATFORM && self.options.bundling { // "bun" is not a real module. It's just globalThis.Bun. // // transform from: @@ -2443,6 +2443,14 @@ pub mod __gated_printer { // const foo = await Promise.resolve(globalThis.Bun) // const bar = globalThis.Bun // + // This inlining is safe only when `bundling` is true, because + // that path always runs a renamer seeded with `globalThis` / + // `Promise` (compute_initial_reserved_names), so a user-declared + // local of either name has already been renamed away. The + // runtime transpiler uses NoOpRenamer and would be captured by + // a local `let globalThis`; let require("bun") / import("bun") + // fall through to the external paths below instead, where the + // runtime module loader resolves "bun" to the Bun object. if record.tag == ImportRecordTag::Bun { if record.kind == ImportKind::Dynamic { self.print(b"Promise.resolve(globalThis.Bun)"); @@ -5761,7 +5769,10 @@ pub mod __gated_printer { self.print_space_before_identifier(); self.add_source_mapping(stmt.loc); - if IS_BUN_PLATFORM { + if IS_BUN_PLATFORM && self.options.bundling { + // Gated on `bundling` for the same reason as the Tag::Bun + // arm in print_require_or_import_expr: the `globalThis.Bun` + // literal is only safe once a renamer has reserved the name. if record.tag == ImportRecordTag::Bun { self.print_global_bun_import_statement(s); self.prev_stmt_tag = new_tag; diff --git a/src/js_printer/renamer.rs b/src/js_printer/renamer.rs index 97826318b86a..091d67f3e9bc 100644 --- a/src/js_printer/renamer.rs +++ b/src/js_printer/renamer.rs @@ -1048,7 +1048,11 @@ pub fn compute_initial_reserved_names( let mut names = StringHashMap::::default(); - const EXTRAS: [&[u8]; 2] = [b"Promise", b"Require"]; + // `globalThis` is reserved because the printer can emit the literal + // `globalThis.Bun` for `require("bun")` / `import("bun")` when bundling + // for the Bun target (see print_require_or_import_expr). A user-declared + // `let globalThis` would otherwise shadow it. https://github.com/oven-sh/bun/issues/8058 + const EXTRAS: [&[u8]; 3] = [b"Promise", b"Require", b"globalThis"]; const CJS_NAMES: [&[u8]; 2] = [b"exports", b"module"]; diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index 1f2447a9ac9e..e10b6931ae71 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -43,7 +43,9 @@ bun_core::declare_scope!(cache, visible); /// path reinstates the bug for any previously-cached TLA module (#30887). /// Version 23: `jsx.runtime`/`jsx.development` participate in the features hash, /// and tsconfig `"jsx": "react-jsx"` now emits the production runtime (#4227). -const EXPECTED_VERSION: u32 = 23; +/// Version 24: `require("bun")` / `import("bun")` are no longer rewritten to the +/// literal `globalThis.Bun` in the runtime transpile path (#8058). +const EXPECTED_VERSION: u32 = 24; /// Source files smaller than this are not written to / read from the on-disk /// transpiler cache. Originally 50 KiB, which excluded almost every file in a diff --git a/test/js/bun/resolve/import-meta.test.js b/test/js/bun/resolve/import-meta.test.js index c0b746c52c17..03026db820c2 100644 --- a/test/js/bun/resolve/import-meta.test.js +++ b/test/js/bun/resolve/import-meta.test.js @@ -225,7 +225,11 @@ it('require("bun") works', () => { }); it('import("bun") works', async () => { - expect(await import("bun")).toBe(Bun); + const ns = await import("bun"); + expect(ns.default).toBe(Bun); + expect(ns.serve).toBe(Bun.serve); + // Consistent with a non-constant specifier (see "dynamically import bun" below) + expect(ns).toBe(await import(eval("'bun'"))); }); it("require.resolve with empty options object", () => { diff --git a/test/regression/issue/08058.test.ts b/test/regression/issue/08058.test.ts new file mode 100644 index 000000000000..c818a2b278ec --- /dev/null +++ b/test/regression/issue/08058.test.ts @@ -0,0 +1,122 @@ +// https://github.com/oven-sh/bun/issues/8058 +// +// The printer used to unconditionally rewrite `require("bun")` / `import("bun")` +// to the literal `globalThis.Bun` / `Promise.resolve(globalThis.Bun)`. A local +// `let globalThis` (or `let Promise`) in scope would shadow that literal and +// change the value of the import. + +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; +import { itBundled } from "../../bundler/expectBundled"; + +describe("runtime transpiler", () => { + async function run(src: string) { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", src], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; + } + + test("require('bun') with a local `globalThis` returns the real Bun object", async () => { + const { stdout, stderr, exitCode } = await run( + `{ let globalThis = { Bun: "intercepted" }; console.log(typeof require("bun").serve); }`, + ); + expect(stderr).toBe(""); + expect(stdout).toBe("function\n"); + expect(exitCode).toBe(0); + }); + + test("dynamic import('bun') with local `globalThis` / `Promise` returns the real Bun object", async () => { + const { stdout, stderr, exitCode } = await run( + `{ let globalThis = { Bun: "intercepted" }; let Promise = null;` + + ` import("bun").then(b => console.log(typeof b.serve)); }`, + ); + expect(stderr).toBe(""); + expect(stdout).toBe("function\n"); + expect(exitCode).toBe(0); + }); + + test("static `import * as B from 'bun'` with module-level `let globalThis`", async () => { + using dir = tempDir("issue-08058-static", { + "index.ts": ` + let globalThis = { Bun: "intercepted" }; + import * as B from "bun"; + console.log(typeof B.serve); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe("function\n"); + expect(exitCode).toBe(0); + }); + + test("require('bun') in a .cjs file with a local `globalThis`", async () => { + using dir = tempDir("issue-08058-cjs", { + "index.cjs": ` + module.exports = 1; + { let globalThis = { Bun: "intercepted" }; console.log(typeof require("bun").serve); } + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.cjs"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe("function\n"); + expect(exitCode).toBe(0); + }); +}); + +describe("bundler", () => { + itBundled("bun/require-bun-shadowed-globalThis", { + target: "bun", + files: { + "/entry.ts": /* js */ ` + { + let globalThis = { Bun: "intercepted" }; + const b = require("bun"); + if (typeof b.serve !== "function") throw new Error("require('bun') was shadowed: " + b); + const d = await import("bun"); + if (typeof d.serve !== "function") throw new Error("import('bun') was shadowed: " + d); + console.log("pass"); + } + `, + }, + run: { stdout: "pass" }, + onAfterBundle(api) { + // The user's local binding should have been renamed away. + expect(api.readFile("out.js")).not.toContain(`let globalThis =`); + }, + }); + + itBundled("bun/require-bun-shadowed-globalThis-cjs", { + target: "bun", + format: "cjs", + files: { + "/entry.ts": /* js */ ` + { + let globalThis = { Bun: "intercepted" }; + const b = require("bun"); + if (typeof b.serve !== "function") throw new Error("require('bun') was shadowed: " + b); + console.log("pass"); + } + `, + }, + run: { stdout: "pass" }, + }); +}); From 7ed11a59da1874a6f81f249de046b151ce3d79c5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 16:52:20 +0000 Subject: [PATCH 2/9] keep the var-destructure lowering for static `import ... from 'bun'` 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'). --- src/js_printer/lib.rs | 12 ++++++--- test/regression/issue/08058.test.ts | 40 ++++++++++++++++++----------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index ea1b03e5d2f4..84e2982b64a6 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -5769,10 +5769,14 @@ pub mod __gated_printer { self.print_space_before_identifier(); self.add_source_mapping(stmt.loc); - if IS_BUN_PLATFORM && self.options.bundling { - // Gated on `bundling` for the same reason as the Tag::Bun - // arm in print_require_or_import_expr: the `globalThis.Bun` - // literal is only safe once a renamer has reserved the name. + if IS_BUN_PLATFORM { + // Unlike print_require_or_import_expr this is not gated on + // `bundling`: lowering `import { x } from "bun"` to a real + // ESM import in the runtime path would (a) eagerly reify + // every property on the Bun object and (b) fail link-time + // validation for names that exist only as types in bun.d.ts. + // When bundling, the renamer has already renamed any user + // `globalThis` away (compute_initial_reserved_names). if record.tag == ImportRecordTag::Bun { self.print_global_bun_import_statement(s); self.prev_stmt_tag = new_tag; diff --git a/test/regression/issue/08058.test.ts b/test/regression/issue/08058.test.ts index c818a2b278ec..c24a440726c9 100644 --- a/test/regression/issue/08058.test.ts +++ b/test/regression/issue/08058.test.ts @@ -40,16 +40,15 @@ describe("runtime transpiler", () => { expect(exitCode).toBe(0); }); - test("static `import * as B from 'bun'` with module-level `let globalThis`", async () => { - using dir = tempDir("issue-08058-static", { - "index.ts": ` - let globalThis = { Bun: "intercepted" }; - import * as B from "bun"; - console.log(typeof B.serve); + test("require('bun') in a .cjs file with a local `globalThis`", async () => { + using dir = tempDir("issue-08058-cjs", { + "index.cjs": ` + module.exports = 1; + { let globalThis = { Bun: "intercepted" }; console.log(typeof require("bun").serve); } `, }); await using proc = Bun.spawn({ - cmd: [bunExe(), "index.ts"], + cmd: [bunExe(), "index.cjs"], env: bunEnv, cwd: String(dir), stdout: "pipe", @@ -61,15 +60,21 @@ describe("runtime transpiler", () => { expect(exitCode).toBe(0); }); - test("require('bun') in a .cjs file with a local `globalThis`", async () => { - using dir = tempDir("issue-08058-cjs", { - "index.cjs": ` - module.exports = 1; - { let globalThis = { Bun: "intercepted" }; console.log(typeof require("bun").serve); } + test("`import { env } from 'bun'` does not eagerly reify the Bun object", async () => { + // `import X from "bun"` is still lowered to `var X = globalThis.Bun` at runtime + // (not a real ESM import) so that (a) only the requested property is touched and + // (b) type-only names declared in bun.d.ts don't fail link-time export validation. + using dir = tempDir("issue-08058-reify", { + "index.ts": ` + import { env } from "bun"; + import { hasNonReifiedStatic } from "bun:internal-for-testing"; + if (!hasNonReifiedStatic(Bun)) throw new Error("import { env } from 'bun' reified the whole Bun object"); + void env; + console.log("pass"); `, }); await using proc = Bun.spawn({ - cmd: [bunExe(), "index.cjs"], + cmd: [bunExe(), "index.ts"], env: bunEnv, cwd: String(dir), stdout: "pipe", @@ -77,7 +82,7 @@ describe("runtime transpiler", () => { }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(stderr).toBe(""); - expect(stdout).toBe("function\n"); + expect(stdout).toBe("pass\n"); expect(exitCode).toBe(0); }); }); @@ -87,6 +92,9 @@ describe("bundler", () => { target: "bun", files: { "/entry.ts": /* js */ ` + import * as B from "bun"; + var globalThis = { Bun: "intercepted" }; + if (typeof B.serve !== "function") throw new Error("import * from 'bun' was shadowed: " + B); { let globalThis = { Bun: "intercepted" }; const b = require("bun"); @@ -100,7 +108,7 @@ describe("bundler", () => { run: { stdout: "pass" }, onAfterBundle(api) { // The user's local binding should have been renamed away. - expect(api.readFile("out.js")).not.toContain(`let globalThis =`); + expect(api.readFile("out.js")).not.toContain(`globalThis = { Bun`); }, }); @@ -109,10 +117,12 @@ describe("bundler", () => { format: "cjs", files: { "/entry.ts": /* js */ ` + import * as B from "bun"; { let globalThis = { Bun: "intercepted" }; const b = require("bun"); if (typeof b.serve !== "function") throw new Error("require('bun') was shadowed: " + b); + if (typeof B.serve !== "function") throw new Error("import * from 'bun' was shadowed: " + B); console.log("pass"); } `, From d6996814bf4fcef4954d46a1678a83cfd2a58d95 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:31:52 +0000 Subject: [PATCH 3/9] shorten inline comments --- src/js_printer/lib.rs | 21 ++++++--------------- src/js_printer/renamer.rs | 5 +---- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index 84e2982b64a6..3e6a931c624a 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -2443,14 +2443,9 @@ pub mod __gated_printer { // const foo = await Promise.resolve(globalThis.Bun) // const bar = globalThis.Bun // - // This inlining is safe only when `bundling` is true, because - // that path always runs a renamer seeded with `globalThis` / - // `Promise` (compute_initial_reserved_names), so a user-declared - // local of either name has already been renamed away. The - // runtime transpiler uses NoOpRenamer and would be captured by - // a local `let globalThis`; let require("bun") / import("bun") - // fall through to the external paths below instead, where the - // runtime module loader resolves "bun" to the Bun object. + // Gated on `bundling`: the runtime transpiler uses NoOpRenamer, so a + // user `let globalThis` would capture the literal (#8058). Fall through + // to the external require/import paths there instead. if record.tag == ImportRecordTag::Bun { if record.kind == ImportKind::Dynamic { self.print(b"Promise.resolve(globalThis.Bun)"); @@ -5770,13 +5765,9 @@ pub mod __gated_printer { self.add_source_mapping(stmt.loc); if IS_BUN_PLATFORM { - // Unlike print_require_or_import_expr this is not gated on - // `bundling`: lowering `import { x } from "bun"` to a real - // ESM import in the runtime path would (a) eagerly reify - // every property on the Bun object and (b) fail link-time - // validation for names that exist only as types in bun.d.ts. - // When bundling, the renamer has already renamed any user - // `globalThis` away (compute_initial_reserved_names). + // Not gated on `bundling` (cf. print_require_or_import_expr): a real + // ESM `import {x} from "bun"` reifies every Bun property and rejects + // type-only names at link time. if record.tag == ImportRecordTag::Bun { self.print_global_bun_import_statement(s); self.prev_stmt_tag = new_tag; diff --git a/src/js_printer/renamer.rs b/src/js_printer/renamer.rs index 091d67f3e9bc..75d7324bc28f 100644 --- a/src/js_printer/renamer.rs +++ b/src/js_printer/renamer.rs @@ -1048,10 +1048,7 @@ pub fn compute_initial_reserved_names( let mut names = StringHashMap::::default(); - // `globalThis` is reserved because the printer can emit the literal - // `globalThis.Bun` for `require("bun")` / `import("bun")` when bundling - // for the Bun target (see print_require_or_import_expr). A user-declared - // `let globalThis` would otherwise shadow it. https://github.com/oven-sh/bun/issues/8058 + // Identifiers the printer may emit as raw text with no Ref (#8058). const EXTRAS: [&[u8]; 3] = [b"Promise", b"Require", b"globalThis"]; const CJS_NAMES: [&[u8]; 2] = [b"exports", b"module"]; From 24e027af04ed8a540bafb1f8a6d829befeaaac88 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:23:13 +0000 Subject: [PATCH 4/9] lower static `import ... from 'bun'` via import.meta.require at runtime; 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. --- src/js_printer/lib.rs | 15 ++- test/bundler/bundler_bun.test.ts | 40 +++++++ test/js/bun/resolve/import-meta.test.js | 82 ++++++++++++++- test/regression/issue/08058.test.ts | 132 ------------------------ 4 files changed, 131 insertions(+), 138 deletions(-) delete mode 100644 test/regression/issue/08058.test.ts diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index 3e6a931c624a..02619bc1bdf9 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -1719,7 +1719,17 @@ pub mod __gated_printer { if !IS_BUN_PLATFORM { unreachable!(); } - self.print_internal_bun_import(import, Some(b"globalThis.Bun")); + let src: &'static [u8] = if self.options.bundling { + b"globalThis.Bun" + } else { + // Runtime: NoOpRenamer can't protect a bare `globalThis`, and + // `import` is ESM-only so `import.meta` is always valid here. + if let Some(mi) = self.module_info() { + mi.flags.contains_import_meta = true; + } + b"import.meta.require(\"bun\")" + }; + self.print_internal_bun_import(import, Some(src)); } fn print_internal_bun_import( @@ -5765,9 +5775,6 @@ pub mod __gated_printer { self.add_source_mapping(stmt.loc); if IS_BUN_PLATFORM { - // Not gated on `bundling` (cf. print_require_or_import_expr): a real - // ESM `import {x} from "bun"` reifies every Bun property and rejects - // type-only names at link time. if record.tag == ImportRecordTag::Bun { self.print_global_bun_import_statement(s); self.prev_stmt_tag = new_tag; diff --git a/test/bundler/bundler_bun.test.ts b/test/bundler/bundler_bun.test.ts index 674da11059bb..3744f49736ca 100644 --- a/test/bundler/bundler_bun.test.ts +++ b/test/bundler/bundler_bun.test.ts @@ -3,6 +3,46 @@ import { describe, expect } from "bun:test"; import { itBundled } from "./expectBundled"; describe("bundler", () => { + // https://github.com/oven-sh/bun/issues/8058 + itBundled("bun/require-bun-shadowed-globalThis", { + target: "bun", + files: { + "/entry.ts": /* js */ ` + import * as B from "bun"; + var globalThis = { Bun: "intercepted" }; + if (typeof B.serve !== "function") throw new Error("import * from 'bun' was shadowed: " + B); + { + let globalThis = { Bun: "intercepted" }; + const b = require("bun"); + if (typeof b.serve !== "function") throw new Error("require('bun') was shadowed: " + b); + const d = await import("bun"); + if (typeof d.serve !== "function") throw new Error("import('bun') was shadowed: " + d); + console.log("pass"); + } + `, + }, + run: { stdout: "pass" }, + onAfterBundle(api) { + expect(api.readFile("out.js")).not.toContain(`globalThis = { Bun`); + }, + }); + itBundled("bun/require-bun-shadowed-globalThis-cjs", { + target: "bun", + format: "cjs", + files: { + "/entry.ts": /* js */ ` + import * as B from "bun"; + { + let globalThis = { Bun: "intercepted" }; + const b = require("bun"); + if (typeof b.serve !== "function") throw new Error("require('bun') was shadowed: " + b); + if (typeof B.serve !== "function") throw new Error("import * from 'bun' was shadowed: " + B); + console.log("pass"); + } + `, + }, + run: { stdout: "pass" }, + }); // https://github.com/oven-sh/bun/issues/18899 itBundled("bun/import-bun-format-cjs", { target: "bun", diff --git a/test/js/bun/resolve/import-meta.test.js b/test/js/bun/resolve/import-meta.test.js index 03026db820c2..85a35a56747a 100644 --- a/test/js/bun/resolve/import-meta.test.js +++ b/test/js/bun/resolve/import-meta.test.js @@ -1,7 +1,7 @@ import { spawnSync } from "bun"; import { isModuleResolveFilenameSlowPathEnabled } from "bun:internal-for-testing"; -import { expect, it, mock } from "bun:test"; -import { bunEnv, bunExe, ospath } from "harness"; +import { describe, expect, it, mock } from "bun:test"; +import { bunEnv, bunExe, ospath, tempDir } from "harness"; import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import Module from "node:module"; import { tmpdir } from "node:os"; @@ -240,6 +240,84 @@ it("dynamically import bun", async () => { expect((await import(eval("'bun'"))).default).toBe(Bun); }); +// https://github.com/oven-sh/bun/issues/8058 +describe("require/import 'bun' with a local `globalThis` in scope", () => { + async function run(files) { + const entry = Object.keys(files)[0]; + using dir = tempDir("issue-08058", files); + await using proc = Bun.spawn({ + cmd: [bunExe(), entry], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe("function\n"); + expect(exitCode).toBe(0); + } + + it("require('bun')", async () => { + await run({ + "index.ts": `{ let globalThis = { Bun: "intercepted" }; console.log(typeof require("bun").serve); }`, + }); + }); + + it("dynamic import('bun') with local `globalThis` and `Promise`", async () => { + await run({ + "index.ts": + `{ let globalThis = { Bun: "intercepted" }; let Promise = null;` + + ` import("bun").then(b => console.log(typeof b.serve)); }`, + }); + }); + + it("require('bun') in CommonJS", async () => { + await run({ + "index.cjs": ` + module.exports = 1; + { let globalThis = { Bun: "intercepted" }; console.log(typeof require("bun").serve); } + `, + }); + }); + + it("static `import * as B from 'bun'` with a module-level `let globalThis`", async () => { + await run({ + "index.ts": ` + let globalThis = { Bun: "intercepted" }; + import * as B from "bun"; + console.log(typeof B.serve); + `, + }); + }); + + it("static `import { env } from 'bun'` does not eagerly reify the Bun object", async () => { + // The import statement form is still lowered to a var-destructure (not a + // real ESM import) so that only the requested property is touched and + // type-only names declared in bun.d.ts don't fail link-time validation. + using dir = tempDir("issue-08058-reify", { + "index.ts": ` + import { env } from "bun"; + import { hasNonReifiedStatic } from "bun:internal-for-testing"; + if (!hasNonReifiedStatic(Bun)) throw new Error("import { env } from 'bun' reified the whole Bun object"); + void env; + console.log("pass"); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe("pass\n"); + expect(exitCode).toBe(0); + }); +}); + it("require.resolve error code", () => { try { require.resolve("node:missing"); diff --git a/test/regression/issue/08058.test.ts b/test/regression/issue/08058.test.ts deleted file mode 100644 index c24a440726c9..000000000000 --- a/test/regression/issue/08058.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -// https://github.com/oven-sh/bun/issues/8058 -// -// The printer used to unconditionally rewrite `require("bun")` / `import("bun")` -// to the literal `globalThis.Bun` / `Promise.resolve(globalThis.Bun)`. A local -// `let globalThis` (or `let Promise`) in scope would shadow that literal and -// change the value of the import. - -import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, tempDir } from "harness"; -import { itBundled } from "../../bundler/expectBundled"; - -describe("runtime transpiler", () => { - async function run(src: string) { - await using proc = Bun.spawn({ - cmd: [bunExe(), "-e", src], - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - return { stdout, stderr, exitCode }; - } - - test("require('bun') with a local `globalThis` returns the real Bun object", async () => { - const { stdout, stderr, exitCode } = await run( - `{ let globalThis = { Bun: "intercepted" }; console.log(typeof require("bun").serve); }`, - ); - expect(stderr).toBe(""); - expect(stdout).toBe("function\n"); - expect(exitCode).toBe(0); - }); - - test("dynamic import('bun') with local `globalThis` / `Promise` returns the real Bun object", async () => { - const { stdout, stderr, exitCode } = await run( - `{ let globalThis = { Bun: "intercepted" }; let Promise = null;` + - ` import("bun").then(b => console.log(typeof b.serve)); }`, - ); - expect(stderr).toBe(""); - expect(stdout).toBe("function\n"); - expect(exitCode).toBe(0); - }); - - test("require('bun') in a .cjs file with a local `globalThis`", async () => { - using dir = tempDir("issue-08058-cjs", { - "index.cjs": ` - module.exports = 1; - { let globalThis = { Bun: "intercepted" }; console.log(typeof require("bun").serve); } - `, - }); - await using proc = Bun.spawn({ - cmd: [bunExe(), "index.cjs"], - env: bunEnv, - cwd: String(dir), - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toBe(""); - expect(stdout).toBe("function\n"); - expect(exitCode).toBe(0); - }); - - test("`import { env } from 'bun'` does not eagerly reify the Bun object", async () => { - // `import X from "bun"` is still lowered to `var X = globalThis.Bun` at runtime - // (not a real ESM import) so that (a) only the requested property is touched and - // (b) type-only names declared in bun.d.ts don't fail link-time export validation. - using dir = tempDir("issue-08058-reify", { - "index.ts": ` - import { env } from "bun"; - import { hasNonReifiedStatic } from "bun:internal-for-testing"; - if (!hasNonReifiedStatic(Bun)) throw new Error("import { env } from 'bun' reified the whole Bun object"); - void env; - console.log("pass"); - `, - }); - await using proc = Bun.spawn({ - cmd: [bunExe(), "index.ts"], - env: bunEnv, - cwd: String(dir), - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toBe(""); - expect(stdout).toBe("pass\n"); - expect(exitCode).toBe(0); - }); -}); - -describe("bundler", () => { - itBundled("bun/require-bun-shadowed-globalThis", { - target: "bun", - files: { - "/entry.ts": /* js */ ` - import * as B from "bun"; - var globalThis = { Bun: "intercepted" }; - if (typeof B.serve !== "function") throw new Error("import * from 'bun' was shadowed: " + B); - { - let globalThis = { Bun: "intercepted" }; - const b = require("bun"); - if (typeof b.serve !== "function") throw new Error("require('bun') was shadowed: " + b); - const d = await import("bun"); - if (typeof d.serve !== "function") throw new Error("import('bun') was shadowed: " + d); - console.log("pass"); - } - `, - }, - run: { stdout: "pass" }, - onAfterBundle(api) { - // The user's local binding should have been renamed away. - expect(api.readFile("out.js")).not.toContain(`globalThis = { Bun`); - }, - }); - - itBundled("bun/require-bun-shadowed-globalThis-cjs", { - target: "bun", - format: "cjs", - files: { - "/entry.ts": /* js */ ` - import * as B from "bun"; - { - let globalThis = { Bun: "intercepted" }; - const b = require("bun"); - if (typeof b.serve !== "function") throw new Error("require('bun') was shadowed: " + b); - if (typeof B.serve !== "function") throw new Error("import * from 'bun' was shadowed: " + B); - console.log("pass"); - } - `, - }, - run: { stdout: "pass" }, - }); -}); From 5253395f2721e35bf1aa327349253a239757e841 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:43:02 +0000 Subject: [PATCH 5/9] test: run subprocess-spawning #8058 cases concurrently --- test/js/bun/resolve/import-meta.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/bun/resolve/import-meta.test.js b/test/js/bun/resolve/import-meta.test.js index 85a35a56747a..89af7b26d0dc 100644 --- a/test/js/bun/resolve/import-meta.test.js +++ b/test/js/bun/resolve/import-meta.test.js @@ -241,7 +241,7 @@ it("dynamically import bun", async () => { }); // https://github.com/oven-sh/bun/issues/8058 -describe("require/import 'bun' with a local `globalThis` in scope", () => { +describe.concurrent("require/import 'bun' with a local `globalThis` in scope", () => { async function run(files) { const entry = Object.keys(files)[0]; using dir = tempDir("issue-08058", files); From f5cb2b2dfd57a49acac655008787bd89577a824f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 21:14:46 +0000 Subject: [PATCH 6/9] ci: retrigger From b559d256b7c27f867bd41feb1bae54081c8f0dba Mon Sep 17 00:00:00 2001 From: robobun Date: Thu, 13 Aug 2026 06:28:37 +0000 Subject: [PATCH 7/9] bundler: also reserve Error/Infinity/NaN/undefined for printer literals (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. --- src/js_printer/renamer.rs | 13 +++- test/bundler/bundler_bun.test.ts | 116 +++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/src/js_printer/renamer.rs b/src/js_printer/renamer.rs index 99ea6037db67..011bfe98cfb0 100644 --- a/src/js_printer/renamer.rs +++ b/src/js_printer/renamer.rs @@ -1056,8 +1056,17 @@ pub fn compute_initial_reserved_names( let mut names = StringHashMap::::default(); - // Identifiers the printer may emit as raw text with no Ref (#8058). - const EXTRAS: [&[u8]; 3] = [b"Promise", b"Require", b"globalThis"]; + // Identifiers the printer may emit as raw text with no Ref (#8058); locals + // with these names get renamed out of the way. + const EXTRAS: [&[u8]; 7] = [ + b"Promise", + b"Require", + b"globalThis", + b"Error", + b"Infinity", + b"NaN", + b"undefined", + ]; const CJS_NAMES: [&[u8]; 2] = [b"exports", b"module"]; diff --git a/test/bundler/bundler_bun.test.ts b/test/bundler/bundler_bun.test.ts index 3744f49736ca..caabafc89c8d 100644 --- a/test/bundler/bundler_bun.test.ts +++ b/test/bundler/bundler_bun.test.ts @@ -43,6 +43,122 @@ describe("bundler", () => { }, run: { stdout: "pass" }, }); + // Locals named after identifiers the printer emits as raw text (globalThis, Error, Infinity, NaN, + // undefined) must be renamed out of the way by the bundler (#8058). + for (const minifyIdentifiers of [false, true]) { + const suffix = minifyIdentifiers ? "Minified" : ""; + itBundled(`bun/RequireBunWithShadowedGlobalThis${suffix}`, { + target: "bun", + minifyIdentifiers, + files: { + "/entry.ts": /* js */ ` + { + let globalThis = { Bun: "intercepted" }; + const b = require("bun"); + if (b === "intercepted") throw new Error("require('bun') captured local globalThis"); + if (typeof b.version !== "string") throw new Error("require('bun') did not return Bun"); + void globalThis; + } + console.log("PASS"); + `, + }, + run: { stdout: "PASS" }, + }); + itBundled(`bun/DynamicImportBunWithShadowedGlobalThis${suffix}`, { + target: "bun", + minifyIdentifiers, + files: { + "/entry.ts": /* js */ ` + (async () => { + let globalThis = { Bun: "intercepted" }; + const b = await import("bun"); + if (b === "intercepted") throw new Error("import('bun') captured local globalThis"); + if (typeof b.version !== "string") throw new Error("import('bun') did not return Bun"); + void globalThis; + console.log("PASS"); + })(); + `, + }, + run: { stdout: "PASS" }, + }); + itBundled(`bun/ImportBunWithShadowedGlobalThis${suffix}`, { + target: "bun", + minifyIdentifiers, + files: { + "/entry.ts": /* js */ ` + import * as b from "bun"; + var globalThis = { Bun: "intercepted" }; + console.log((globalThis as any).Bun); + if ((b as any) === "intercepted") throw new Error("import 'bun' captured local globalThis"); + if (typeof b?.version !== "string") throw new Error("import 'bun' did not return Bun"); + console.log("PASS"); + `, + }, + run: { stdout: "intercepted\nPASS" }, + }); + } + itBundled("bun/InlinedRequireErrorWithShadowedError", { + target: "bun", + files: { + "/entry.ts": /* js */ ` + { + let Error = function (this: any, msg: string) { this.intercepted = true; this.message = msg; }; + let caught: any; + try { require("does-not-exist-pkg") } catch (e) { caught = e; } + console.log(typeof Error); + if (caught.intercepted) throw new globalThis.Error("require shim captured local Error"); + if (!(caught instanceof globalThis.Error)) throw new globalThis.Error("not a real Error"); + } + console.log("PASS"); + `, + }, + run: { stdout: "function\nPASS" }, + }); + itBundled("bun/InfinityLiteralWithShadowedInfinity", { + target: "bun", + files: { + "/entry.ts": /* js */ ` + { + let Infinity = 5; + let trap = Infinity; + if (1e400 === trap) throw new Error("Infinity literal captured local Infinity"); + if (1e400 !== globalThis.Infinity) throw new Error("1e400 is not Infinity"); + } + console.log("PASS"); + `, + }, + run: { stdout: "PASS" }, + }); + itBundled("bun/NaNLiteralWithShadowedNaN", { + target: "bun", + minifySyntax: true, + files: { + "/entry.ts": /* js */ ` + function check(NaN: any) { + if (!globalThis.Number.isNaN(0/0)) throw new Error("folded NaN captured local NaN"); + return NaN; + } + console.log(check(6) === 6 ? "PASS" : "FAIL"); + `, + }, + run: { stdout: "PASS" }, + }); + itBundled("bun/SynthesizedUndefinedWithShadowedUndefined", { + target: "bun", + files: { + "/entry.ts": /* js */ ` + { + let undefined = 5; + let trap = undefined; + const h = import.meta.hot; + if (h === trap) throw new Error("synthesized undefined captured local"); + if (h !== globalThis.undefined) throw new Error("import.meta.hot is not undefined"); + } + console.log("PASS"); + `, + }, + run: { stdout: "PASS" }, + }); // https://github.com/oven-sh/bun/issues/18899 itBundled("bun/import-bun-format-cjs", { target: "bun", From 7af5142d248063986250d9c7007b2584ad62a450 Mon Sep 17 00:00:00 2001 From: robobun Date: Thu, 13 Aug 2026 06:28:37 +0000 Subject: [PATCH 8/9] test: update the builtin-esm-lazy-exports comments for the runtime "bun" import lowering --- test/js/bun/resolve/builtin-esm-lazy-exports.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/js/bun/resolve/builtin-esm-lazy-exports.test.ts b/test/js/bun/resolve/builtin-esm-lazy-exports.test.ts index d82c66e201f6..4b5b0cc43a28 100644 --- a/test/js/bun/resolve/builtin-esm-lazy-exports.test.ts +++ b/test/js/bun/resolve/builtin-esm-lazy-exports.test.ts @@ -43,9 +43,9 @@ const helper = ` // static entries without constructing them; for...in constructs all of them, so the entries below avoid it.) The // watched names are a sample of those entries plus the plain function (`write`, `createRequire`) an entry imports. // -// Note that a literal `import ... from "bun"` (and `import("bun")` / `require("bun")`) is rewritten by the -// transpiler into a read of globalThis.Bun and never loads the module; the module is what `export ... from "bun"` -// and a non-literal import() specifier go through. Imports of node:process and node:module always load the module. +// Note that a static `import ... from "bun"` and `require("bun")` resolve to the Bun object without loading the +// module; the module is what `export ... from "bun"` and import() go through. Imports of node:process and +// node:module always load the module. const WATCHED = ["$", "CryptoHasher", "Glob", "S3Client", "SQL", "TOML", "Transpiler", "secrets", "write"] as const; const PROCESS_WATCHED = ["allowedNodeEnvironmentFlags", "config", "release", "stderr", "stdin", "stdout", "versions"]; const MODULE_WATCHED = [ @@ -74,8 +74,7 @@ const nativeHelper = ` export function print(result) { console.log(JSON.stringify(result)); } - // import(specifier) with this really loads the module; a literal (or a const the transpiler can inline) would be - // rewritten to globalThis.Bun instead. + // import(specifier) loads the module regardless of how the transpiler treats a literal import("bun"). export const specifier = "bun"; `; From c96c43c88fd62a9b4bfb37c574e324169a2cdf77 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 13 Aug 2026 08:52:31 +0000 Subject: [PATCH 9/9] test: inspector module graph now lists "bun" in cjs 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. --- test/cli/inspect/BunFrontendDevServer.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/cli/inspect/BunFrontendDevServer.test.ts b/test/cli/inspect/BunFrontendDevServer.test.ts index af083513f938..971c2eaa0054 100644 --- a/test/cli/inspect/BunFrontendDevServer.test.ts +++ b/test/cli/inspect/BunFrontendDevServer.test.ts @@ -358,13 +358,16 @@ describe.if(isPosix)("BunFrontendDevServer inspector protocol", () => { moduleGraph.esm = moduleGraph.esm.map(a => a.replaceAll("\\", "/").replaceAll(realCwd, "")); moduleGraph.main = moduleGraph.main.replaceAll("\\", "/").replaceAll(realCwd, ""); moduleGraph.cwd = moduleGraph.cwd.replaceAll("\\", "/").replaceAll(realCwd, ""); + // "bun" is in cjs because server.ts's `import { serve } from "bun"` is lowered to a require("bun"). expect(moduleGraph).toMatchInlineSnapshot(` { "argv": [ "${path.basename(process.execPath)}", "server.ts", ], - "cjs": [], + "cjs": [ + "bun", + ], "cwd": "", "esm": [ "/index.html",