Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -1801,7 +1801,17 @@ pub(crate) 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 @@ -2474,7 +2484,7 @@ pub(crate) 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 @@ -2485,6 +2495,9 @@ pub(crate) 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
12 changes: 11 additions & 1 deletion src/js_printer/renamer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1056,7 +1056,17 @@ 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); locals
// with these names get renamed out of the way.
Comment thread
robobun marked this conversation as resolved.
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"];

Expand Down
4 changes: 3 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ bun_core::declare_scope!(cache, visible);
/// Version 25: Every ModuleInfo record carries a trailing FetchParameters slot
/// so ImportEntry/ExportEntry/StarExportEntry moduleRequestType matches JSC's
/// after WebKit 90b2ecf79ae3 keyed m_loadedModules on (specifier, type).
const EXPECTED_VERSION: u32 = 25;
/// Version 26: `require("bun")` / `import("bun")` / `import ... from "bun"` are no
/// longer rewritten to the literal `globalThis.Bun` in the runtime transpile path (#8058).
Comment thread
robobun marked this conversation as resolved.
const EXPECTED_VERSION: u32 = 26;

/// 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
156 changes: 156 additions & 0 deletions test/bundler/bundler_bun.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,162 @@ 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" },
});
// 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",
Expand Down
5 changes: 4 additions & 1 deletion test/cli/inspect/BunFrontendDevServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,13 +358,16 @@ describe.if(isPosix)("BunFrontendDevServer inspector protocol", () => {
moduleGraph.esm = moduleGraph.esm.map(a => a.replaceAll("\\", "/").replaceAll(realCwd, "<cwd>"));
moduleGraph.main = moduleGraph.main.replaceAll("\\", "/").replaceAll(realCwd, "<cwd>");
moduleGraph.cwd = moduleGraph.cwd.replaceAll("\\", "/").replaceAll(realCwd, "<cwd>");
// "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": "<cwd>",
"esm": [
"<cwd>/index.html",
Expand Down
9 changes: 4 additions & 5 deletions test/js/bun/resolve/builtin-esm-lazy-exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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";
`;

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('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 All @@ -236,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.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);
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");
Expand Down