mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Add a regression test for GitHub issue #26653 which reported that transitive dependencies could incorrectly use real filesystem paths instead of virtual /$bunfs/ paths in standalone builds when a plugin transforms files. The test verifies that when a plugin's onLoad callback transforms a file, the transitive dependencies (files imported by the transformed file) still correctly have import.meta.url pointing to the virtual filesystem path rather than the original filesystem path. Testing shows the basic behavior is correct - this test ensures it stays that way. The original issue may only manifest in very large codebases (70+ transformed files). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1000 lines
32 KiB
TypeScript
1000 lines
32 KiB
TypeScript
import { Database } from "bun:sqlite";
|
||
import { describe, expect, test } from "bun:test";
|
||
import { rmSync } from "fs";
|
||
import { bunEnv, bunExe, isWindows, tempDir, tempDirWithFiles } from "harness";
|
||
import { join } from "path";
|
||
import { itBundled } from "./expectBundled";
|
||
|
||
describe("bundler", () => {
|
||
itBundled("compile/HelloWorld", {
|
||
compile: true,
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
console.log("Hello, world!");
|
||
`,
|
||
},
|
||
run: { stdout: "Hello, world!" },
|
||
});
|
||
itBundled("compile/HelloWorldWithProcessVersionsBun", {
|
||
compile: true,
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
process.exitCode = 1;
|
||
process.versions.bun = "bun!";
|
||
if (process.versions.bun === "bun!") throw new Error("fail");
|
||
if (require("./${process.platform}-${process.arch}.js") === "${Bun.version.replaceAll("-debug", "")}") {
|
||
process.exitCode = 0;
|
||
}
|
||
`,
|
||
[`/${process.platform}-${process.arch}.js`]: "module.exports = process.versions.bun;",
|
||
},
|
||
run: { exitCode: 0 },
|
||
});
|
||
itBundled("compile/HelloWorldWithProcessVersionsBunAPI", {
|
||
compile: true,
|
||
backend: "api",
|
||
outfile: "dist/out",
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
import { foo } from "hello:world";
|
||
if (foo !== "bar") throw new Error("fail");
|
||
process.exitCode = 1;
|
||
process.versions.bun = "bun!";
|
||
if (process.versions.bun === "bun!") throw new Error("fail");
|
||
const another = require("./${process.platform}-${process.arch}.js").replaceAll("-debug", "");
|
||
if (another === "${Bun.version.replaceAll("-debug", "")}") {
|
||
process.exitCode = 0;
|
||
}
|
||
`,
|
||
[`/${process.platform}-${process.arch}.js`]: "module.exports = process.versions.bun;",
|
||
},
|
||
run: { exitCode: 0, stdout: "hello world" },
|
||
plugins: [
|
||
{
|
||
name: "hello-world",
|
||
setup(api) {
|
||
api.onResolve({ filter: /hello:world/, namespace: "file" }, args => {
|
||
return {
|
||
path: args.path,
|
||
namespace: "hello",
|
||
};
|
||
});
|
||
api.onLoad({ filter: /.*/, namespace: "hello" }, args => {
|
||
return {
|
||
contents: "export const foo = 'bar'; console.log('hello world');",
|
||
loader: "js",
|
||
};
|
||
});
|
||
},
|
||
},
|
||
],
|
||
});
|
||
itBundled("compile/HelloWorldBytecode", {
|
||
compile: true,
|
||
bytecode: true,
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
console.log("Hello, world!");
|
||
`,
|
||
},
|
||
run: {
|
||
stdout: "Hello, world!",
|
||
stderr: [
|
||
"[Disk Cache] Cache hit for sourceCode",
|
||
|
||
// TODO: remove this line once bun:main is removed.
|
||
"[Disk Cache] Cache miss for sourceCode",
|
||
].join("\n"),
|
||
env: {
|
||
BUN_JSC_verboseDiskCache: "1",
|
||
},
|
||
},
|
||
});
|
||
// 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,
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
import bar from './foo.file' with {type: "file"};
|
||
if ((await Bun.file(bar).text()).trim() !== "abcd") throw "fail";
|
||
console.log("Hello, world!");
|
||
`,
|
||
"/foo.file": /* js */ `
|
||
abcd
|
||
`.trim(),
|
||
},
|
||
outfile: "dist/out",
|
||
run: { stdout: "Hello, world!" },
|
||
});
|
||
itBundled("compile/WorkerRelativePathNoExtension", {
|
||
backend: "cli",
|
||
compile: true,
|
||
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");
|
||
`,
|
||
"/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 },
|
||
});
|
||
itBundled("compile/WorkerRelativePathTSExtension", {
|
||
backend: "cli",
|
||
compile: true,
|
||
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 },
|
||
});
|
||
itBundled("compile/WorkerRelativePathTSExtensionBytecode", {
|
||
backend: "cli",
|
||
compile: true,
|
||
bytecode: true,
|
||
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,
|
||
stderr: [
|
||
"[Disk Cache] Cache hit for sourceCode",
|
||
|
||
// TODO: remove this line once bun:main is removed.
|
||
"[Disk Cache] Cache miss for sourceCode",
|
||
|
||
"[Disk Cache] Cache hit for sourceCode",
|
||
|
||
// TODO: remove this line once bun:main is removed.
|
||
"[Disk Cache] Cache miss for sourceCode",
|
||
].join("\n"),
|
||
env: {
|
||
BUN_JSC_verboseDiskCache: "1",
|
||
},
|
||
},
|
||
});
|
||
itBundled("compile/Bun.embeddedFiles", {
|
||
compile: true,
|
||
// TODO: this shouldn't be necessary, or we should add a map aliasing files.
|
||
assetNaming: "[name].[ext]",
|
||
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
import {rmSync} from 'fs';
|
||
import {createRequire} from 'module';
|
||
import './foo.file';
|
||
import './1.embed';
|
||
import './2.embed';
|
||
rmSync('./foo.file', {force: true});
|
||
rmSync('./1.embed', {force: true});
|
||
rmSync('./2.embed', {force: true});
|
||
const names = {
|
||
"1.embed": "1.embed",
|
||
"2.embed": "2.embed",
|
||
"foo.file": "foo.file",
|
||
}
|
||
// We want to verify it omits source code.
|
||
for (let f of Bun.embeddedFiles) {
|
||
const name = f.name;
|
||
if (!names[name]) {
|
||
throw new Error("Unexpected embedded file: " + name);
|
||
}
|
||
}
|
||
|
||
if (Bun.embeddedFiles.length !== 3) throw "fail";
|
||
if ((await Bun.file(createRequire(import.meta.url).resolve('./1.embed')).text()).trim() !== "abcd") throw "fail";
|
||
if ((await Bun.file(createRequire(import.meta.url).resolve('./2.embed')).text()).trim() !== "abcd") throw "fail";
|
||
if ((await Bun.file(createRequire(import.meta.url).resolve('./foo.file')).text()).trim() !== "abcd") throw "fail";
|
||
if ((await Bun.file(import.meta.require.resolve('./1.embed')).text()).trim() !== "abcd") throw "fail";
|
||
if ((await Bun.file(import.meta.require.resolve('./2.embed')).text()).trim() !== "abcd") throw "fail";
|
||
if ((await Bun.file(import.meta.require.resolve('./foo.file')).text()).trim() !== "abcd") throw "fail";
|
||
console.log("Hello, world!");
|
||
`,
|
||
"/1.embed": /* js */ `
|
||
abcd
|
||
`.trim(),
|
||
"/2.embed": /* js */ `
|
||
abcd
|
||
`.trim(),
|
||
"/foo.file": /* js */ `
|
||
abcd
|
||
`.trim(),
|
||
},
|
||
outfile: "dist/out",
|
||
run: { stdout: "Hello, world!", setCwd: true },
|
||
});
|
||
itBundled("compile/ResolveEmbeddedFileOutfile", {
|
||
compile: true,
|
||
// TODO: this shouldn't be necessary, or we should add a map aliasing files.
|
||
assetNaming: "[name].[ext]",
|
||
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
import {rmSync} from 'fs';
|
||
import './foo.file';
|
||
rmSync('./foo.file', {force: true});
|
||
if ((await Bun.file(import.meta.require.resolve('./foo.file')).text()).trim() !== "abcd") throw "fail";
|
||
console.log("Hello, world!");
|
||
`,
|
||
"/foo.file": /* js */ `
|
||
abcd
|
||
`.trim(),
|
||
},
|
||
outfile: "dist/out",
|
||
run: { stdout: "Hello, world!" },
|
||
});
|
||
itBundled("compile/pathToFileURLWorks", {
|
||
compile: true,
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
import {pathToFileURL, fileURLToPath} from 'bun';
|
||
console.log(pathToFileURL(import.meta.path).href + " " + fileURLToPath(import.meta.url));
|
||
if (fileURLToPath(import.meta.url) !== import.meta.path) throw "fail";
|
||
if (pathToFileURL(import.meta.path).href !== import.meta.url) throw "fail";
|
||
`,
|
||
},
|
||
run: {
|
||
stdout:
|
||
process.platform !== "win32"
|
||
? `file:///$bunfs/root/out /$bunfs/root/out`
|
||
: `file:///B:/~BUN/root/out B:\\~BUN\\root\\out`,
|
||
setCwd: true,
|
||
},
|
||
});
|
||
itBundled("compile/VariousBunAPIs", {
|
||
todo: isWindows, // TODO(@paperclover)
|
||
compile: true,
|
||
files: {
|
||
"/entry.ts": `
|
||
// testing random features of bun
|
||
import 'node:process';
|
||
import 'process';
|
||
import 'fs';
|
||
|
||
import { Database } from "bun:sqlite";
|
||
import { serve } from 'bun';
|
||
import { getRandomSeed } from 'bun:jsc';
|
||
const db = new Database("test.db");
|
||
const query = db.query(\`select "Hello world" as message\`);
|
||
if (query.get().message !== "Hello world") throw "fail from sqlite";
|
||
const icon = await fetch("https://bun.sh/favicon.ico").then(x=>x.arrayBuffer())
|
||
if(icon.byteLength < 100) throw "fail from icon";
|
||
if (typeof getRandomSeed() !== 'number') throw "fail from bun:jsc";
|
||
const server = serve({
|
||
fetch() {
|
||
return new Response("Hello world");
|
||
},
|
||
port: 0,
|
||
});
|
||
const res = await fetch(\`http://\${server.hostname}:\${server.port}\`);
|
||
if (res.status !== 200) throw "fail from server";
|
||
if (await res.text() !== "Hello world") throw "fail from server";
|
||
server.stop();
|
||
console.log("ok");
|
||
`,
|
||
},
|
||
run: { stdout: "ok" },
|
||
});
|
||
|
||
const additionalOptionsIters: Array<{
|
||
bytecode?: boolean;
|
||
minify?: boolean;
|
||
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" },
|
||
{ format: "esm", minify: true },
|
||
];
|
||
|
||
for (const additionalOptions of additionalOptionsIters) {
|
||
const { bytecode = false, format, minify = false } = additionalOptions;
|
||
const NODE_ENV = minify ? "'production'" : undefined;
|
||
itBundled("compile/ReactSSR" + (bytecode ? "+bytecode" : "") + "+" + format + (minify ? "+minify" : ""), {
|
||
install: ["react@19.2.0-canary-b94603b9-20250513", "react-dom@19.2.0-canary-b94603b9-20250513"],
|
||
format,
|
||
minifySyntax: minify,
|
||
minifyIdentifiers: minify,
|
||
minifyWhitespace: minify,
|
||
define: NODE_ENV ? { "process.env.NODE_ENV": NODE_ENV } : undefined,
|
||
files: {
|
||
"/entry.tsx": /* tsx */ `
|
||
import React from "react";
|
||
import { renderToReadableStream } from "react-dom/server";
|
||
|
||
const headers = {
|
||
headers: {
|
||
"Content-Type": "text/html",
|
||
},
|
||
};
|
||
|
||
const App = () => (
|
||
<html>
|
||
<body>
|
||
<h1>Hello World</h1>
|
||
<p>This is an example.</p>
|
||
</body>
|
||
</html>
|
||
);
|
||
|
||
async function main() {
|
||
const port = 0;
|
||
using server = Bun.serve({
|
||
port,
|
||
async fetch(req) {
|
||
return new Response(await renderToReadableStream(<App />), headers);
|
||
},
|
||
});
|
||
const res = await fetch(server.url);
|
||
if (res.status !== 200) throw "status error";
|
||
console.log(await res.text());
|
||
}
|
||
|
||
main();
|
||
`,
|
||
},
|
||
run: {
|
||
stdout: "<!DOCTYPE html><html><head></head><body><h1>Hello World</h1><p>This is an example.</p></body></html>",
|
||
stderr: bytecode
|
||
? "[Disk Cache] Cache hit for sourceCode\n[Disk Cache] Cache miss for sourceCode\n"
|
||
: undefined,
|
||
env: bytecode
|
||
? {
|
||
BUN_JSC_verboseDiskCache: "1",
|
||
}
|
||
: undefined,
|
||
},
|
||
compile: true,
|
||
bytecode,
|
||
});
|
||
}
|
||
itBundled("compile/DynamicRequire", {
|
||
files: {
|
||
"/entry.tsx": /* tsx */ `
|
||
const req = (x) => require(x);
|
||
const y = req('commonjs');
|
||
const z = req('esm').default;
|
||
console.log(JSON.stringify([w, x, y, z]));
|
||
module.exports = null;
|
||
`,
|
||
"/node_modules/commonjs/index.js": "throw new Error('Must be runtime import.')",
|
||
"/node_modules/esm/index.js": "throw new Error('Must be runtime import.')",
|
||
"/node_modules/other/index.js": "throw new Error('Must be runtime import.')",
|
||
"/node_modules/other-esm/index.js": "throw new Error('Must be runtime import.')",
|
||
},
|
||
runtimeFiles: {
|
||
"/node_modules/commonjs/index.js": "module.exports = 2; require('other');",
|
||
"/node_modules/esm/index.js": "import 'other-esm'; export default 3;",
|
||
"/node_modules/other/index.js": "globalThis.x = 1;",
|
||
"/node_modules/other-esm/index.js": "globalThis.w = 0;",
|
||
},
|
||
run: {
|
||
stdout: "[0,1,2,3]",
|
||
setCwd: true,
|
||
},
|
||
compile: true,
|
||
});
|
||
itBundled("compile/DynamicImport", {
|
||
files: {
|
||
"/entry.tsx": /* tsx */ `
|
||
import 'static';
|
||
const imp = (x) => import(x).then(x => x.default);
|
||
const y = await imp('commonjs');
|
||
const z = await imp('esm');
|
||
console.log(JSON.stringify([w, x, y, z]));
|
||
`,
|
||
"/node_modules/static/index.js": "'use strict';",
|
||
"/node_modules/commonjs/index.js": "throw new Error('Must be runtime import.')",
|
||
"/node_modules/esm/index.js": "throw new Error('Must be runtime import.')",
|
||
"/node_modules/other/index.js": "throw new Error('Must be runtime import.')",
|
||
"/node_modules/other-esm/index.js": "throw new Error('Must be runtime import.')",
|
||
},
|
||
runtimeFiles: {
|
||
"/node_modules/commonjs/index.js": "module.exports = 2; require('other');",
|
||
"/node_modules/esm/index.js": "import 'other-esm'; export default 3;",
|
||
"/node_modules/other/index.js": "globalThis.x = 1;",
|
||
"/node_modules/other-esm/index.js": "globalThis.w = 0;",
|
||
},
|
||
run: {
|
||
stdout: "[0,1,2,3]",
|
||
setCwd: true,
|
||
},
|
||
compile: true,
|
||
});
|
||
// see comment in `usePackageManager` for why this is a test
|
||
itBundled("compile/NoAutoInstall", {
|
||
files: {
|
||
"/entry.tsx": /* tsx */ `
|
||
const req = (x) => require(x);
|
||
console.log(req('express'));
|
||
`,
|
||
},
|
||
run: {
|
||
error: 'Cannot find package "express"',
|
||
setCwd: true,
|
||
},
|
||
compile: true,
|
||
});
|
||
itBundled("compile/CanRequireLocalPackages", {
|
||
files: {
|
||
"/entry.tsx": /* tsx */ `
|
||
const req = (x) => require(x);
|
||
console.log(req('react/package.json').version);
|
||
`,
|
||
},
|
||
run: {
|
||
stdout: require("react/package.json").version,
|
||
setCwd: false,
|
||
},
|
||
compile: true,
|
||
});
|
||
for (const minify of [true, false] as const) {
|
||
itBundled("compile/platform-specific-binary" + (minify ? "-minify" : ""), {
|
||
minifySyntax: minify,
|
||
target: "bun",
|
||
compile: true,
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
await import(\`./platform.\${process.platform}.\${process.arch}.js\`);
|
||
`,
|
||
[`/platform.${process.platform}.${process.arch}.js`]: `console.log("${process.platform}", "${process.arch}");`,
|
||
},
|
||
run: { stdout: `${process.platform} ${process.arch}` },
|
||
});
|
||
for (const sourceMap of ["external", "inline", "none"] as const) {
|
||
// https://github.com/oven-sh/bun/issues/10344
|
||
itBundled("compile/10344+sourcemap=" + sourceMap + (minify ? "+minify" : ""), {
|
||
minifyIdentifiers: minify,
|
||
minifySyntax: minify,
|
||
minifyWhitespace: minify,
|
||
target: "bun",
|
||
sourceMap,
|
||
compile: true,
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
import big from './generated.big.binary' with {type: "file"};
|
||
import small from './generated.small.binary' with {type: "file"};
|
||
import fs from 'fs';
|
||
fs.readFileSync(big).toString("hex");
|
||
await Bun.file(big).arrayBuffer();
|
||
fs.readFileSync(small).toString("hex");
|
||
if ((await fs.promises.readFile(small)).length !== 31) throw "fail readFile";
|
||
if (fs.statSync(small).size !== 31) throw "fail statSync";
|
||
if (fs.statSync(big).size !== (4096 + (32 - 2))) throw "fail statSync";
|
||
if (((await fs.promises.stat(big)).size) !== (4096 + (32 - 2))) throw "fail stat";
|
||
await Bun.file(small).arrayBuffer();
|
||
console.log("PASS");
|
||
`,
|
||
"/generated.big.binary": (() => {
|
||
// make sure the size is not divisible by 32
|
||
const buffer = new Uint8ClampedArray(4096 + (32 - 2));
|
||
for (let i = 0; i < buffer.length; i++) {
|
||
buffer[i] = i;
|
||
}
|
||
return buffer;
|
||
})(),
|
||
"/generated.small.binary": (() => {
|
||
// make sure the size is less than 32
|
||
const buffer = new Uint8ClampedArray(31);
|
||
for (let i = 0; i < buffer.length; i++) {
|
||
buffer[i] = i;
|
||
}
|
||
return buffer;
|
||
})(),
|
||
},
|
||
run: { stdout: "PASS" },
|
||
});
|
||
}
|
||
}
|
||
itBundled("compile/EmbeddedSqlite", {
|
||
compile: true,
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
import db from './db.sqlite' with {type: "sqlite", embed: "true"};
|
||
console.log(db.query("select message from messages LIMIT 1").get().message);
|
||
`,
|
||
"/db.sqlite": (() => {
|
||
const db = new Database(":memory:");
|
||
db.exec("create table messages (message text)");
|
||
db.exec("insert into messages values ('Hello, world!')");
|
||
return db.serialize();
|
||
})(),
|
||
},
|
||
run: { stdout: "Hello, world!" },
|
||
});
|
||
itBundled("compile/sqlite-file", {
|
||
compile: true,
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
import db from './db.sqlite' with {type: "sqlite"};
|
||
console.log(db.query("select message from messages LIMIT 1").get().message);
|
||
`,
|
||
},
|
||
runtimeFiles: {
|
||
"/db.sqlite": (() => {
|
||
const db = new Database(":memory:");
|
||
db.exec("create table messages (message text)");
|
||
db.exec("insert into messages values ('Hello, world!')");
|
||
return db.serialize();
|
||
})(),
|
||
},
|
||
run: { stdout: "Hello, world!", setCwd: true },
|
||
});
|
||
itBundled("compile/Utf8", {
|
||
compile: true,
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
console.log(JSON.stringify({\u{6211}: "\u{6211}"}));
|
||
`,
|
||
},
|
||
run: { stdout: '{"\u{6211}":"\u{6211}"}' },
|
||
});
|
||
itBundled("compile/ImportMetaMain", {
|
||
compile: true,
|
||
backend: "cli",
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
// test toString on function to observe what the inlined value was
|
||
console.log((() => import.meta.main).toString().includes('true'));
|
||
console.log((() => !import.meta.main).toString().includes('false'));
|
||
console.log((() => !!import.meta.main).toString().includes('true'));
|
||
console.log((() => require.main == module).toString().includes('true'));
|
||
console.log((() => require.main === module).toString().includes('true'));
|
||
console.log((() => require.main !== module).toString().includes('false'));
|
||
console.log((() => require.main !== module).toString().includes('false'));
|
||
`,
|
||
},
|
||
run: { stdout: new Array(7).fill("true").join("\n") },
|
||
});
|
||
itBundled("compile/SourceMap", {
|
||
target: "bun",
|
||
compile: true,
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
// this file has comments and weird whitespace, intentionally
|
||
// to make it obvious if sourcemaps were generated and mapped properly
|
||
if (true) code();
|
||
function code() {
|
||
// hello world
|
||
throw new
|
||
Error("Hello World");
|
||
}
|
||
`,
|
||
},
|
||
sourceMap: "external",
|
||
onAfterBundle(api) {
|
||
rmSync(api.join("entry.ts"), {}); // Hide the source files for errors
|
||
},
|
||
run: {
|
||
exitCode: 1,
|
||
validate({ stderr }) {
|
||
expect(stderr).toStartWith(
|
||
`1 | // this file has comments and weird whitespace, intentionally
|
||
2 | // to make it obvious if sourcemaps were generated and mapped properly
|
||
3 | if (true) code();
|
||
4 | function code() {
|
||
5 | // hello world
|
||
6 | throw new
|
||
^
|
||
error: Hello World`,
|
||
);
|
||
expect(stderr).toInclude("entry.ts:6:19");
|
||
},
|
||
},
|
||
});
|
||
itBundled("compile/SourceMapBigFile", {
|
||
target: "bun",
|
||
compile: true,
|
||
files: {
|
||
"/entry.ts": /* js */ `import * as ReactDom from ${JSON.stringify(require.resolve("react-dom/server"))};
|
||
|
||
// this file has comments and weird whitespace, intentionally
|
||
// to make it obvious if sourcemaps were generated and mapped properly
|
||
if (true) code();
|
||
function code() {
|
||
// hello world
|
||
throw new
|
||
Error("Hello World");
|
||
}
|
||
|
||
console.log(ReactDom);`,
|
||
},
|
||
sourceMap: "external",
|
||
onAfterBundle(api) {
|
||
rmSync(api.join("entry.ts"), {}); // Hide the source files for errors
|
||
},
|
||
run: {
|
||
exitCode: 1,
|
||
validate({ stderr }) {
|
||
expect(stderr).toStartWith(
|
||
`3 | // this file has comments and weird whitespace, intentionally
|
||
4 | // to make it obvious if sourcemaps were generated and mapped properly
|
||
5 | if (true) code();
|
||
6 | function code() {
|
||
7 | // hello world
|
||
8 | throw new
|
||
^
|
||
error: Hello World`,
|
||
);
|
||
expect(stderr).toInclude("entry.ts:8:19");
|
||
},
|
||
},
|
||
});
|
||
itBundled("compile/BunBeBunEnvVar", {
|
||
compile: true,
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
console.log("This is compiled code");
|
||
`,
|
||
},
|
||
run: [
|
||
{
|
||
stdout: "This is compiled code",
|
||
},
|
||
{
|
||
env: { BUN_BE_BUN: "1" },
|
||
validate({ stdout }) {
|
||
expect(stdout).not.toContain("This is compiled code");
|
||
},
|
||
},
|
||
],
|
||
});
|
||
|
||
test("does not crash", async () => {
|
||
const dir = tempDirWithFiles("bundler-compile-shadcn", {
|
||
"frontend.tsx": `console.log("Hello, world!");`,
|
||
"index.html": `<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Bun + React</title>
|
||
<script type="module" src="./frontend.tsx" async></script>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
</body>
|
||
</html>
|
||
`,
|
||
"index.tsx": `import { serve } from "bun";
|
||
import index from "./index.html";
|
||
|
||
const server = serve({
|
||
routes: {
|
||
// Serve index.html for all unmatched routes.
|
||
"/*": index,
|
||
|
||
"/api/hello": {
|
||
async GET(req) {
|
||
return Response.json({
|
||
message: "Hello, world!",
|
||
method: "GET",
|
||
});
|
||
},
|
||
async PUT(req) {
|
||
return Response.json({
|
||
message: "Hello, world!",
|
||
method: "PUT",
|
||
});
|
||
},
|
||
},
|
||
|
||
"/api/hello/:name": async req => {
|
||
const name = req.params.name;
|
||
return Response.json({
|
||
message: "LOL",
|
||
});
|
||
},
|
||
},
|
||
|
||
development: process.env.NODE_ENV !== "production" && {
|
||
// Enable browser hot reloading in development
|
||
hmr: true,
|
||
|
||
// Echo console logs from the browser to the server
|
||
console: true,
|
||
},
|
||
});
|
||
|
||
`,
|
||
});
|
||
|
||
// Step 2: Run bun build with compile, minify, sourcemap, and bytecode
|
||
await Bun.$`${bunExe()} build ./index.tsx --compile --minify --sourcemap --bytecode`
|
||
.cwd(dir)
|
||
.env(bunEnv)
|
||
.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", {
|
||
"app.js": `console.log("IT WORKS");`,
|
||
"assets/file-1": "",
|
||
"assets/file-2": "",
|
||
"assets/file-3": "",
|
||
"assets/file-4": "",
|
||
"assets/file-5": "",
|
||
"assets/file-6": "",
|
||
"assets/file-7": "",
|
||
"assets/file-8": "",
|
||
});
|
||
|
||
await Bun.$`${bunExe()} build --compile app.js assets/* --outfile app`.cwd(dir).env(bunEnv).throws(true);
|
||
|
||
const result = await Bun.$`./app`.cwd(dir).env(bunEnv).nothrow();
|
||
expect(result.stdout.toString().trim()).toBe("IT WORKS");
|
||
});
|
||
|
||
// Regression test for https://github.com/oven-sh/bun/issues/26653
|
||
// When a plugin transforms a file's content, transitive dependencies should
|
||
// still have correct import.meta.url pointing to the virtual /$bunfs/ path,
|
||
// not the original filesystem path.
|
||
itBundled("compile/PluginTransformPreservesTransitiveImportMetaUrl", {
|
||
compile: true,
|
||
backend: "api",
|
||
outfile: "dist/out",
|
||
files: {
|
||
"/entry.ts": /* js */ `
|
||
import { processData } from "./model.ts";
|
||
const result = processData();
|
||
console.log("Result:", result);
|
||
`,
|
||
"/model.ts": /* js */ `
|
||
// This file is transformed by a plugin - it uses a placeholder
|
||
// that gets replaced with the actual filesystem path
|
||
import { helper } from "./utils.ts";
|
||
const MODEL_URL = "MODEL_URL_PLACEHOLDER";
|
||
export function processData() {
|
||
console.log("Model URL:", MODEL_URL);
|
||
return helper();
|
||
}
|
||
`,
|
||
"/utils.ts": /* js */ `
|
||
// This transitive dependency is NOT transformed by the plugin.
|
||
// Its import.meta.url should point to the virtual /$bunfs/ path,
|
||
// not the original filesystem path.
|
||
export function helper() {
|
||
const url = import.meta.url;
|
||
console.log("Utils URL:", url);
|
||
// Verify the URL is the virtual path
|
||
if (url.includes("/$bunfs/") || url.includes("B:\\\\~BUN\\\\")) {
|
||
return "success";
|
||
} else {
|
||
return "FAIL: import.meta.url is not virtual path: " + url;
|
||
}
|
||
}
|
||
`,
|
||
},
|
||
plugins: [
|
||
{
|
||
name: "transform-model",
|
||
setup(api) {
|
||
api.onLoad({ filter: /model\.ts$/ }, async args => {
|
||
const { pathToFileURL } = await import("url");
|
||
const contents = await Bun.file(args.path).text();
|
||
// Replace placeholder with actual filesystem path
|
||
const fileUrl = pathToFileURL(args.path).href;
|
||
const transformed = contents.replace("MODEL_URL_PLACEHOLDER", fileUrl);
|
||
return { contents: transformed, loader: "ts" };
|
||
});
|
||
},
|
||
},
|
||
],
|
||
run: {
|
||
// The output should show success from utils.ts, verifying import.meta.url is correct
|
||
stdout: /Result: success/,
|
||
},
|
||
});
|
||
});
|