Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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: 15 additions & 2 deletions src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
robobun marked this conversation as resolved.
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(
Expand Down Expand Up @@ -2432,7 +2442,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 +2453,9 @@ pub mod __gated_printer {
// const foo = await Promise.resolve(globalThis.Bun)
// const bar = globalThis.Bun
//
// 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.
Comment thread
robobun marked this conversation as resolved.
if record.tag == ImportRecordTag::Bun {
if record.kind == ImportKind::Dynamic {
self.print(b"Promise.resolve(globalThis.Bun)");
Expand Down
3 changes: 2 additions & 1 deletion src/js_printer/renamer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1048,7 +1048,8 @@ pub fn compute_initial_reserved_names(

let mut names = StringHashMap::<u32>::default();

const EXTRAS: [&[u8]; 2] = [b"Promise", b"Require"];
// 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"];

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
40 changes: 40 additions & 0 deletions test/bundler/bundler_bun.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
88 changes: 85 additions & 3 deletions test/js/bun/resolve/import-meta.test.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -225,7 +225,11 @@
});

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 All @@ -236,6 +240,84 @@
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);
}

Check warning on line 259 in test/js/bun/resolve/import-meta.test.js

View check run for this annotation

Claude / Claude Code Review

New subprocess-spawning tests should use describe.concurrent

These five new tests each spawn an independent subprocess in an isolated `tempDir` with no shared state, so this block can be `describe.concurrent` (per REVIEW.md harness conventions: "`test.concurrent` for independent subprocess suites"). Serial spawns compound under debug+ASAN's 10-100× slowdown; running them concurrently is free wall-clock.
Comment thread
robobun marked this conversation as resolved.
Outdated

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");
Expand Down
Loading