Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment thread
robobun marked this conversation as resolved.
// "bun" is not a real module. It's just globalThis.Bun.
//
// transform from:
Expand All @@ -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.
Comment thread
robobun marked this conversation as resolved.
Outdated
if record.tag == ImportRecordTag::Bun {
if record.kind == ImportKind::Dynamic {
self.print(b"Promise.resolve(globalThis.Bun)");
Expand Down Expand Up @@ -5762,6 +5770,13 @@ 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).
Comment thread
robobun marked this conversation as resolved.
Outdated
if record.tag == ImportRecordTag::Bun {
self.print_global_bun_import_statement(s);
self.prev_stmt_tag = new_tag;
Expand Down
6 changes: 5 additions & 1 deletion src/js_printer/renamer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1048,7 +1048,11 @@ pub fn compute_initial_reserved_names(

let mut names = StringHashMap::<u32>::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
Comment thread
robobun marked this conversation as resolved.
Outdated
const EXTRAS: [&[u8]; 3] = [b"Promise", b"Require", b"globalThis"];

const CJS_NAMES: [&[u8]; 2] = [b"exports", b"module"];

Expand Down
4 changes: 3 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Comment thread
robobun marked this conversation as resolved.
Outdated
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
Expand Down
6 changes: 5 additions & 1 deletion test/js/bun/resolve/import-meta.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
132 changes: 132 additions & 0 deletions test/regression/issue/08058.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { itBundled } from "../../bundler/expectBundled";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

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", {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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" },
});
});
Loading