esm bytecode (#26402)

### What does this PR do?

### How did you verify your code works?

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This commit is contained in:
Alistair Smith
2026-01-30 01:38:45 -08:00
committed by GitHub
parent 8f61adf494
commit 71ce550cfa
34 changed files with 2572 additions and 145 deletions

View File

@@ -1,7 +1,8 @@
import { Database } from "bun:sqlite";
import { describe, expect, test } from "bun:test";
import { rmSync } from "fs";
import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness";
import { bunEnv, bunExe, isWindows, tempDir, tempDirWithFiles } from "harness";
import { join } from "path";
import { itBundled } from "./expectBundled";
describe("bundler", () => {
@@ -89,6 +90,135 @@ describe("bundler", () => {
},
},
});
// ESM bytecode test matrix: each scenario × {default, minified} = 2 tests per scenario.
// With --compile, static imports are inlined into one chunk, but dynamic imports
// create separate modules in the standalone graph — each with its own bytecode + ModuleInfo.
const esmBytecodeScenarios: Array<{
name: string;
files: Record<string, string>;
stdout: string;
}> = [
{
name: "HelloWorld",
files: {
"/entry.ts": `console.log("Hello, world!");`,
},
stdout: "Hello, world!",
},
{
// top-level await is ESM-only; if ModuleInfo or bytecode generation
// mishandles async modules, this breaks.
name: "TopLevelAwait",
files: {
"/entry.ts": `
const result = await Promise.resolve("tla works");
console.log(result);
`,
},
stdout: "tla works",
},
{
// import.meta is ESM-only.
name: "ImportMeta",
files: {
"/entry.ts": `
console.log(typeof import.meta.url === "string" ? "ok" : "fail");
console.log(typeof import.meta.dir === "string" ? "ok" : "fail");
`,
},
stdout: "ok\nok",
},
{
// Dynamic import creates a separate module in the standalone graph,
// exercising per-module bytecode + ModuleInfo.
name: "DynamicImport",
files: {
"/entry.ts": `
const { value } = await import("./lazy.ts");
console.log("lazy:", value);
`,
"/lazy.ts": `export const value = 42;`,
},
stdout: "lazy: 42",
},
{
// Dynamic import of a module that itself uses top-level await.
// The dynamically imported module is a separate chunk with async
// evaluation — stresses both ModuleInfo and async bytecode loading.
name: "DynamicImportTLA",
files: {
"/entry.ts": `
const mod = await import("./async-mod.ts");
console.log("value:", mod.value);
`,
"/async-mod.ts": `export const value = await Promise.resolve(99);`,
},
stdout: "value: 99",
},
{
// Multiple dynamic imports: several separate modules in the graph,
// each with its own bytecode + ModuleInfo.
name: "MultipleDynamicImports",
files: {
"/entry.ts": `
const [a, b] = await Promise.all([
import("./mod-a.ts"),
import("./mod-b.ts"),
]);
console.log(a.value, b.value);
`,
"/mod-a.ts": `export const value = "a";`,
"/mod-b.ts": `export const value = "b";`,
},
stdout: "a b",
},
];
for (const scenario of esmBytecodeScenarios) {
for (const minify of [false, true]) {
itBundled(`compile/ESMBytecode+${scenario.name}${minify ? "+minify" : ""}`, {
compile: true,
bytecode: true,
format: "esm",
...(minify && {
minifySyntax: true,
minifyIdentifiers: true,
minifyWhitespace: true,
}),
files: scenario.files,
run: { stdout: scenario.stdout },
});
}
}
// Multi-entry ESM bytecode with Worker (can't be in the matrix — needs
// entryPointsRaw, outfile, setCwd). Each entry becomes a separate module
// in the standalone graph with its own bytecode + ModuleInfo.
itBundled("compile/WorkerBytecodeESM", {
backend: "cli",
compile: true,
bytecode: true,
format: "esm",
files: {
"/entry.ts": /* js */ `
import {rmSync} from 'fs';
// Verify we're not just importing from the filesystem
rmSync("./worker.ts", {force: true});
console.log("Hello, world!");
new Worker("./worker.ts");
`,
"/worker.ts": /* js */ `
console.log("Worker loaded!");
`.trim(),
},
entryPointsRaw: ["./entry.ts", "./worker.ts"],
outfile: "dist/out",
run: {
stdout: "Hello, world!\nWorker loaded!\n",
file: "dist/out",
setCwd: true,
},
});
// https://github.com/oven-sh/bun/issues/8697
itBundled("compile/EmbeddedFileOutfile", {
compile: true,
@@ -311,6 +441,8 @@ describe("bundler", () => {
format: "cjs" | "esm";
}> = [
{ bytecode: true, minify: true, format: "cjs" },
{ bytecode: true, format: "esm" },
{ bytecode: true, minify: true, format: "esm" },
{ format: "cjs" },
{ format: "cjs", minify: true },
{ format: "esm" },
@@ -736,6 +868,54 @@ const server = serve({
.throws(true);
});
// Verify ESM bytecode is actually loaded from the cache at runtime, not just generated.
// Uses regex matching on stderr (not itBundled) since we don't know the exact
// number of cache hit/miss lines for ESM standalone.
test("ESM bytecode cache is used at runtime", async () => {
const ext = isWindows ? ".exe" : "";
using dir = tempDir("esm-bytecode-cache", {
"entry.js": `console.log("esm bytecode loaded");`,
});
const outfile = join(String(dir), `app${ext}`);
// Build with ESM + bytecode
await using build = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
"--bytecode",
"--format=esm",
join(String(dir), "entry.js"),
"--outfile",
outfile,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [, buildStderr, buildExitCode] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]);
expect(buildStderr).toBe("");
expect(buildExitCode).toBe(0);
// Run with verbose disk cache to verify bytecode is loaded
await using exe = Bun.spawn({
cmd: [outfile],
env: { ...bunEnv, BUN_JSC_verboseDiskCache: "1" },
stdout: "pipe",
stderr: "pipe",
});
const [exeStdout, exeStderr, exeExitCode] = await Promise.all([exe.stdout.text(), exe.stderr.text(), exe.exited]);
expect(exeStdout).toContain("esm bytecode loaded");
expect(exeStderr).toMatch(/\[Disk Cache\].*Cache hit/i);
expect(exeExitCode).toBe(0);
});
// When compiling with 8+ entry points, the main entry point should still run correctly.
test("compile with 8+ entry points runs main entry correctly", async () => {
const dir = tempDirWithFiles("compile-many-entries", {

View File

@@ -36,5 +36,30 @@ describe("bundler", () => {
stdout: "app entry\nheader rendering\nmenu showing\nitems: home,about,contact",
},
});
for (const minify of [false, true]) {
itBundled(`compile/splitting/ImportMetaInSplitChunk${minify ? "+minify" : ""}`, {
compile: true,
splitting: true,
bytecode: true,
format: "esm",
...(minify ? { minifySyntax: true, minifyIdentifiers: true, minifyWhitespace: true } : {}),
files: {
"/entry.ts": /* js */ `
const mod = await import("./worker.ts");
mod.run();
`,
"/worker.ts": /* js */ `
export function run() {
console.log(typeof import.meta.url === "string" ? "ok" : "fail");
console.log(typeof import.meta.dir === "string" ? "ok" : "fail");
}
`,
},
run: {
stdout: "ok\nok",
},
});
}
});
});

View File

@@ -163,8 +163,8 @@ describe.skipIf(!isWindows).concurrent("Windows compile metadata", () => {
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(exitCode).not.toBe(0);
// When cross-compiling to non-Windows, it tries to download the target but fails
expect(stderr.toLowerCase()).toContain("target platform");
// Windows flags require a Windows compile target
expect(stderr.toLowerCase()).toContain("windows compile target");
});
});

View File

@@ -1,4 +1,4 @@
import { expect, test } from "bun:test" with { todo: "true" };
import { expect, test } from "bun:test";
import "reflect-metadata";
function Abc() {
return (target: any, field: string) => {};

View File

@@ -0,0 +1,499 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness";
const ext = isWindows ? ".exe" : "";
function compileAndRun(dir: string, entrypoint: string) {
const outfile = dir + `/compiled${ext}`;
const buildResult = Bun.spawnSync({
cmd: [bunExe(), "build", "--compile", "--bytecode", "--format=esm", entrypoint, "--outfile", outfile],
env: bunEnv,
cwd: dir,
stdio: ["inherit", "pipe", "pipe"],
});
expect(buildResult.stderr.toString()).toBe("");
expect(buildResult.exitCode).toBe(0);
return Bun.spawnSync({
cmd: [outfile],
env: bunEnv,
cwd: dir,
stdio: ["inherit", "pipe", "pipe"],
});
}
const a_file = `
export type my_string = "1";
export type my_value = "2";
export const my_value = "2";
export const my_only = "3";
`;
const a_no_value = `
export type my_string = "1";
export type my_value = "2";
export const my_only = "3";
`;
const a_with_value = `
export type my_string = "1";
export const my_value = "2";
`;
const b_files = [
{
name: "export from",
value: `export { my_string, my_value, my_only } from "./a.ts";`,
},
{
name: "import then export",
value: `
import { my_string, my_value, my_only } from "./a.ts";
export { my_string, my_value, my_only };
`,
},
{
name: "export star",
value: `export * from "./a.ts";`,
},
{
name: "export merge",
value: `export * from "./a_no_value.ts"; export * from "./a_with_value.ts"`,
},
];
const c_files = [
{ name: "require", value: `console.log(JSON.stringify(require("./b")));` },
{ name: "import star", value: `import * as b from "./b"; console.log(JSON.stringify(b));` },
{ name: "await import", value: `console.log(JSON.stringify(await import("./b")));` },
{
name: "import individual",
value: `
import { my_string, my_value, my_only } from "./b";
console.log(JSON.stringify({ my_only, my_value }));
`,
},
];
for (const b_file of b_files) {
describe(`re-export with ${b_file.name}`, () => {
for (const c_file of c_files) {
describe(`import with ${c_file.name}`, () => {
const dir = tempDirWithFiles("type-export", {
"a.ts": a_file,
"b.ts": b_file.value,
"c.ts": c_file.value,
"a_no_value.ts": a_no_value,
"a_with_value.ts": a_with_value,
});
describe.each(["run", "compile", "build"])("%s", mode => {
// TODO: "run" is skipped until ESM module_info is enabled in the runtime transpiler.
// Currently module_info is only generated for standalone ESM bytecode (--compile).
// Once enabled, flip this to include "run".
test.skipIf(mode === "run")("works", async () => {
let result: Bun.SyncSubprocess<"pipe", "inherit"> | Bun.SyncSubprocess<"pipe", "pipe">;
if (mode === "compile") {
result = compileAndRun(dir, dir + "/c.ts");
} else if (mode === "build") {
const build_result = await Bun.build({
entrypoints: [dir + "/c.ts"],
outdir: dir + "/dist",
});
expect(build_result.success).toBe(true);
result = Bun.spawnSync({
cmd: [bunExe(), "run", dir + "/dist/c.js"],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "inherit"],
});
} else {
result = Bun.spawnSync({
cmd: [bunExe(), "run", "c.ts"],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "inherit"],
});
}
const parsedOutput = JSON.parse(result.stdout.toString().trim());
expect(parsedOutput).toEqual({ my_value: "2", my_only: "3" });
expect(result.exitCode).toBe(0);
});
});
});
}
});
}
describe("import not found", () => {
for (const [ccase, target_value, name] of [
[``, /SyntaxError: Export named 'not_found' not found in module '[^']+?'\./, "none"],
[
`export default function not_found() {};`,
/SyntaxError: Export named 'not_found' not found in module '[^']+?'\. Did you mean to import default\?/,
"default with same name",
],
[
`export type not_found = "not_found";`,
/SyntaxError: Export named 'not_found' not found in module '[^']+?'\./,
"type",
],
] as const)
test(`${name}`, () => {
const dir = tempDirWithFiles("type-export", {
"a.ts": ccase,
"b.ts": /*js*/ `
import { not_found } from "./a";
console.log(not_found);
`,
"nf.ts": "",
});
const result = Bun.spawnSync({
cmd: [bunExe(), "run", "b.ts"],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(result.stderr?.toString().trim()).toMatch(target_value);
expect({
exitCode: result.exitCode,
stdout: result.stdout?.toString().trim(),
}).toEqual({
exitCode: 1,
stdout: "",
});
});
});
test("js file type export", () => {
const dir = tempDirWithFiles("type-export", {
"a.js": "export {not_found};",
});
const result = Bun.spawnSync({
cmd: [bunExe(), "a.js"],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(result.stderr?.toString().trim()).toInclude('error: "not_found" is not declared in this file');
expect(result.exitCode).toBe(1);
});
test("js file type import", () => {
const dir = tempDirWithFiles("type-import", {
"b.js": "import {type_only} from './ts.ts';",
"ts.ts": "export type type_only = 'type_only';",
});
const result = Bun.spawnSync({
cmd: [bunExe(), "b.js"],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(result.stderr?.toString().trim()).toInclude("Export named 'type_only' not found in module '");
expect(result.stderr?.toString().trim()).not.toInclude("Did you mean to import default?");
expect(result.exitCode).toBe(1);
});
test("js file type import with default export", () => {
const dir = tempDirWithFiles("type-import", {
"b.js": "import {type_only} from './ts.ts';",
"ts.ts": "export type type_only = 'type_only'; export default function type_only() {};",
});
const result = Bun.spawnSync({
cmd: [bunExe(), "b.js"],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(result.stderr?.toString().trim()).toInclude("Export named 'type_only' not found in module '");
expect(result.stderr?.toString().trim()).toInclude("Did you mean to import default?");
expect(result.exitCode).toBe(1);
});
test("js file with through export", () => {
const dir = tempDirWithFiles("type-import", {
"b.js": "export {type_only} from './ts.ts';",
"ts.ts": "export type type_only = 'type_only'; export default function type_only() {};",
});
const result = Bun.spawnSync({
cmd: [bunExe(), "b.js"],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(result.stderr?.toString().trim()).toInclude("SyntaxError: export 'type_only' not found in './ts.ts'");
expect(result.exitCode).toBe(1);
});
test("js file with through export 2", () => {
const dir = tempDirWithFiles("type-import", {
"b.js": "import {type_only} from './ts.ts'; export {type_only};",
"ts.ts": "export type type_only = 'type_only'; export default function type_only() {};",
});
const result = Bun.spawnSync({
cmd: [bunExe(), "b.js"],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(result.stderr?.toString().trim()).toInclude("SyntaxError: export 'type_only' not found in './ts.ts'");
expect(result.exitCode).toBe(1);
});
describe("through export merge", () => {
// this isn't allowed, even in typescript (tsc emits "Duplicate identifier 'value'.")
for (const fmt of ["js", "ts"]) {
describe(fmt, () => {
for (const [name, mode] of [
["through", "export {value} from './b'; export {value} from './c';"],
["direct", "export {value} from './b'; export const value = 'abc';"],
["direct2", "export const value = 'abc'; export {value};"],
["ns", "export * as value from './c'; export * as value from './c';"],
]) {
describe(name, () => {
const dir = tempDirWithFiles("type-import", {
["main." + fmt]: "import {value} from './a'; console.log(value);",
["a." + fmt]: mode,
["b." + fmt]: fmt === "ts" ? "export type value = 'b';" : "",
["c." + fmt]: "export const value = 'c';",
});
for (const file of ["main." + fmt, "a." + fmt]) {
test(file, () => {
const result = Bun.spawnSync({
cmd: [bunExe(), file],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(result.stderr?.toString().trim()).toInclude(
file === "a." + fmt
? 'error: Multiple exports with the same name "value"\n' // bun's syntax error
: "SyntaxError: Cannot export a duplicate name 'value'.\n", // jsc's syntax error
);
expect(result.exitCode).toBe(1);
});
}
});
}
});
}
});
describe("check ownkeys from a star import", () => {
const dir = tempDirWithFiles("ownkeys-star-import", {
["main.ts"]: `
import * as ns from './a';
console.log(JSON.stringify({
keys: Object.keys(ns).sort(),
ns,
has_sometype: Object.hasOwn(ns, 'sometype'),
}));
`,
["a.ts"]: "export * from './b'; export {sometype} from './b';",
["b.ts"]: "export const value = 'b'; export const anotherValue = 'another'; export type sometype = 'sometype';",
});
const expected = {
keys: ["anotherValue", "value"],
ns: {
anotherValue: "another",
value: "b",
},
has_sometype: false,
};
describe.each(["run", "compile"] as const)("%s", mode => {
const testFn = mode === "run" ? test.skip : test;
testFn("works", () => {
const result =
mode === "compile"
? compileAndRun(dir, dir + "/main.ts")
: Bun.spawnSync({
cmd: [bunExe(), "main.ts"],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(result.stderr?.toString().trim()).toBe("");
expect(JSON.parse(result.stdout?.toString().trim())).toEqual(expected);
expect(result.exitCode).toBe(0);
});
});
});
test("check commonjs", () => {
const dir = tempDirWithFiles("commonjs", {
["main.ts"]: "const {my_value, my_type} = require('./a'); console.log(my_value, my_type);",
["a.ts"]: "module.exports = require('./b');",
["b.ts"]: "export const my_value = 'my_value'; export type my_type = 'my_type';",
});
const result = Bun.spawnSync({
cmd: [bunExe(), "main.ts"],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(result.stderr?.toString().trim()).toBe("");
expect(result.stdout?.toString().trim()).toBe("my_value undefined");
expect(result.exitCode).toBe(0);
});
test("check merge", () => {
const dir = tempDirWithFiles("merge", {
["main.ts"]: "import {value} from './a'; console.log(value);",
["a.ts"]: "export * from './b'; export * from './c';",
["b.ts"]: "export const value = 'b';",
["c.ts"]: "export const value = 'c';",
});
const result = Bun.spawnSync({
cmd: [bunExe(), "main.ts"],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(result.stderr?.toString().trim()).toInclude(
"SyntaxError: Export named 'value' cannot be resolved due to ambiguous multiple bindings in module",
);
expect(result.exitCode).toBe(1);
});
describe("export * from './module'", () => {
for (const fmt of ["js", "ts"]) {
describe(fmt, () => {
const dir = tempDirWithFiles("export-star", {
["main." + fmt]: "import {value} from './a'; console.log(value);",
["a." + fmt]: "export * from './b';",
["b." + fmt]: "export const value = 'b';",
});
for (const file of ["main." + fmt, "a." + fmt]) {
test(file, () => {
const result = Bun.spawnSync({
cmd: [bunExe(), file],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(result.stderr?.toString().trim()).toBe("");
expect(result.exitCode).toBe(0);
});
}
});
}
});
describe("export * as ns from './module'", () => {
for (const fmt of ["js", "ts"]) {
describe(fmt, () => {
const dir = tempDirWithFiles("export-star-as", {
["main." + fmt]: "import {ns} from './a'; console.log(ns.value);",
["a." + fmt]: "export * as ns from './b';",
["b." + fmt]: "export const value = 'b';",
});
for (const file of ["main." + fmt, "a." + fmt]) {
test(file, () => {
const result = Bun.spawnSync({
cmd: [bunExe(), file],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(result.stderr?.toString().trim()).toBe("");
expect(result.exitCode).toBe(0);
});
}
});
}
});
describe("export type {Type} from './module'", () => {
for (const fmt of ["ts"]) {
describe(fmt, () => {
const dir = tempDirWithFiles("export-type", {
["main." + fmt]: "import {Type} from './a'; const x: Type = 'test'; console.log(x);",
["a." + fmt]: "export type {Type} from './b';",
["b." + fmt]: "export type Type = string;",
});
for (const file of ["main." + fmt, "a." + fmt]) {
test(file, () => {
const result = Bun.spawnSync({
cmd: [bunExe(), file],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(result.stderr?.toString().trim()).toBe("");
expect(result.exitCode).toBe(0);
});
}
});
}
});
describe("import only used in decorator (#8439)", () => {
const dir = tempDirWithFiles("import-only-used-in-decorator", {
["index.ts"]: /*js*/ `
import { TestInterface } from "./interface.ts";
function Decorator(): PropertyDecorator {
return () => {};
}
class TestClass {
@Decorator()
test?: TestInterface;
}
class OtherClass {
other?: TestInterface;
}
export {TestInterface};
`,
["interface.ts"]: "export interface TestInterface {};",
"tsconfig.json": JSON.stringify({
compilerOptions: {
experimentalDecorators: true,
emitDecoratorMetadata: true,
},
}),
});
describe.each(["run", "compile"] as const)("%s", mode => {
const testFn = mode === "run" ? test.skip : test;
testFn("works", () => {
const result =
mode === "compile"
? compileAndRun(dir, dir + "/index.ts")
: Bun.spawnSync({
cmd: [bunExe(), "index.ts"],
cwd: dir,
env: bunEnv,
stdio: ["inherit", "pipe", "pipe"],
});
expect(result.stderr?.toString().trim()).toBe("");
expect(result.exitCode).toBe(0);
});
});
});