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
34 changes: 10 additions & 24 deletions src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2475,31 +2475,17 @@ pub(crate) mod __gated_printer {
let module_type = self.options.module_type;

if IS_BUN_PLATFORM {
// "bun" is not a real module. It's just globalThis.Bun.
//
// transform from:
// const foo = await import("bun")
// const bar = require("bun")
//
// transform to:
// const foo = await Promise.resolve(globalThis.Bun)
// const bar = globalThis.Bun
//
if record.tag == ImportRecordTag::Bun {
if record.kind == ImportKind::Dynamic {
self.print(b"Promise.resolve(globalThis.Bun)");
if wrap {
self.print(b")");
}
return;
} else if record.kind == ImportKind::Require || record.kind == ImportKind::Stmt
{
self.print(b"globalThis.Bun");
if wrap {
self.print(b")");
}
return;
// import("bun") is excluded: the loader returns the module namespace for it,
// and a specifier that escapes const inlining reaches the loader anyway, so
// the literal form has to resolve to that same namespace.
Comment thread
robobun marked this conversation as resolved.
if record.tag == ImportRecordTag::Bun
&& (record.kind == ImportKind::Require || record.kind == ImportKind::Stmt)
Comment thread
claude[bot] marked this conversation as resolved.
{
self.print(b"globalThis.Bun");
if wrap {
self.print(b")");
}
return;
}
}

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: a literal `import("bun")` is printed as a dynamic import instead of
/// `Promise.resolve(globalThis.Bun)`.
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
35 changes: 35 additions & 0 deletions test/bundler/bundler_bun.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,41 @@ import { describe, expect } from "bun:test";
import { itBundled } from "./expectBundled";

describe("bundler", () => {
// import("bun") is left for the runtime, which resolves it to the "bun" module
// namespace (default = Bun plus a named export per property), the same thing a
// non-literal specifier gets. require("bun") is still inlined to globalThis.Bun,
// which is also what the runtime returns for a non-literal require().
itBundled("bun/dynamic-import-bun-is-module-namespace", {
target: "bun",
files: {
"/entry.ts": /* js */ `
const ns = await import("bun");
console.log(Object.prototype.toString.call(ns), ns.default === Bun, ns.serve === Bun.serve);
console.log(require("bun") === Bun);
`,
},
run: { stdout: "[object Module] true true\ntrue" },
onAfterBundle(api) {
api.expectFile("out.js").toContain('import("bun")');
api.expectFile("out.js").not.toContain("Promise.resolve(globalThis.Bun)");
},
});
// --minify-syntax inlines the const, so the import() reaches the printer as a literal.
itBundled("bun/dynamic-import-bun-inlined-const", {
target: "bun",
minifySyntax: true,
files: {
"/entry.ts": /* js */ `
const specifier = "bun";
const ns = await import(specifier);
console.log(Object.prototype.toString.call(ns), ns.default === Bun, ns.serve === Bun.serve);
`,
},
run: { stdout: "[object Module] true true" },
onAfterBundle(api) {
api.expectFile("out.js").toContain('import("bun")');
},
});
// https://github.com/oven-sh/bun/issues/18899
itBundled("bun/import-bun-format-cjs", {
target: "bun",
Expand Down
13 changes: 5 additions & 8 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")` are rewritten by the transpiler into a read of
// globalThis.Bun and never load 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,9 +74,6 @@ 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.
export const specifier = "bun";
`;

const bunReexport = `
Expand Down Expand Up @@ -329,8 +326,8 @@ test.concurrent('"bun": re-exports construct the properties that get bound, not

test.concurrent('"bun": import() namespace has the same export list, and every export is the Bun.* value', async () => {
const result = await runEntry(`
import { constructed, print, specifier } from "./native-helper.mjs";
const ns = await import(specifier);
import { constructed, print } from "./native-helper.mjs";
const ns = await import("bun");
const afterImport = constructed();
// Neither [[OwnPropertyKeys]] of the namespace nor Object.keys of the Bun object reads any of the properties.
const exportNames = Reflect.ownKeys(ns).filter(key => typeof key === "string").sort();
Expand Down
43 changes: 39 additions & 4 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 @@ -224,8 +224,43 @@ it('require("bun") works', () => {
expect(require("bun")).toBe(Bun);
});

it('import("bun") works', async () => {
expect(await import("bun")).toBe(Bun);
it('import("bun") returns the "bun" module namespace', async () => {
const ns = await import("bun");
expect(ns.default).toBe(Bun);
expect(ns.serve).toBe(Bun.serve);
// Same module record as a specifier the transpiler cannot see through.
expect(ns).toBe(await import(eval("'bun'")));
});

// Whether `import(specifier)` reaches the printer as a literal depends on const
// inlining, which only applies to a const declared before any other statement in
// its scope. What comes back must not depend on that.
describe.concurrent('import() of "bun" resolves to the same thing however the specifier is written', () => {
it.each([
["string literal", `const ns = await import("bun");`],
["const declared first (inlined)", `const specifier = "bun"; const ns = await import(specifier);`],
[
"const declared after an import (not inlined)",
`import "./empty.mjs"; const specifier = "bun"; const ns = await import(specifier);`,
],
["let", `let specifier = "bun"; const ns = await import(specifier);`],
])("%s", async (_, source) => {
using dir = tempDir("import-bun", {
"empty.mjs": "",
"entry.mjs": `${source}\nconsole.log(Object.prototype.toString.call(ns), ns.default === Bun, ns.serve === Bun.serve);\n`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "entry.mjs"],
cwd: String(dir),
env: bunEnv,
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("[object Module] true true\n");
expect(exitCode).toBe(0);
});
});

it("require.resolve with empty options object", () => {
Expand Down
31 changes: 31 additions & 0 deletions test/js/bun/test/mock/mock-module.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// - Write test for import {foo} from "./foo"; export {foo}

import { expect, mock, spyOn, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { default as defaultValue, fn, iCallFn, rexported, rexportedAs, variable } from "./mock-module-fixture";
import * as spyFixture from "./spymodule-fixture";

Expand Down Expand Up @@ -166,3 +167,33 @@ test("mocking a builtin", async () => {
const { readFile } = await import("node:fs/promises");
expect(await readFile("hello.txt", "utf8")).toBe("hello world");
});

// Runs in a child so the mock of "bun" does not outlive this test. A literal
// import("bun") used to be rewritten by the transpiler and skip the module
// registry, so only the non-literal form saw the mock.
test('mocking "bun" applies to a literal import("bun") like it does to a computed specifier', async () => {
using dir = tempDir("mock-bun-module", {
"mock-bun.test.ts": `
import { expect, mock, test } from "bun:test";

test("literal and computed import() of bun agree", async () => {
mock.module("bun", () => ({ mocked: true }));
const literal = await import("bun");
const computed = await import(eval("'bun'"));
expect(literal.mocked).toBe(true);
expect(literal).toBe(computed);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "mock-bun.test.ts"],
cwd: String(dir),
env: bunEnv,
stdout: "ignore",
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).toContain(" 1 pass\n");
expect(stderr).toContain(" 0 fail\n");
expect(exitCode).toBe(0);
});