Files

411 lines
11 KiB
TypeScript
Raw Permalink Normal View History

2026-08-27 21:09:14 +00:00
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { ESBUILD, itBundled } from "./expectBundled";
describe("bundler", () => {
itBundled("naming/EntryNamingCollission", {
files: {
"/a/entry.js": /* js */ `
console.log(1);
`,
"/b/entry.js": /* js */ `
console.log(2);
`,
},
entryNaming: "[name].[ext]",
entryPointsRaw: ["./a/entry.js", "./b/entry.js"],
bundleErrors: {
// expectBundled does not support newlines.
"<bun>": [`Multiple files share the same output path`],
},
});
itBundled("naming/ImplicitOutbase1", {
files: {
"/a/entry.js": /* js */ `
console.log(1);
`,
"/b/entry.js": /* js */ `
console.log(2);
`,
},
entryPointsRaw: ["./a/entry.js", "./b/entry.js"],
run: [
{
file: "/out/a/entry.js",
stdout: "1",
},
{
file: "/out/b/entry.js",
stdout: "2",
},
],
});
itBundled("naming/ImplicitOutbase2", {
files: {
"/a/hello/entry.js": /* js */ `
import data from '../dependency'
console.log(data);
`,
"/a/dependency.js": /* js */ `
export default 1;
`,
"/a/hello/world/entry.js": /* js */ `
console.log(2);
`,
"/a/hello/world/a/a/a/a/a/a/a/entry.js": /* js */ `
console.log(3);
`,
},
entryPointsRaw: ["./a/hello/entry.js", "./a/hello/world/entry.js", "./a/hello/world/a/a/a/a/a/a/a/entry.js"],
run: [
{
file: "/out/entry.js",
stdout: "1",
},
{
file: "/out/world/entry.js",
stdout: "2",
},
{
file: "/out/world/a/a/a/a/a/a/a/entry.js",
stdout: "3",
},
],
});
itBundled("naming/EntryNamingTemplate1", {
files: {
"/a/hello/entry.js": /* js */ `
import data from '../dependency'
console.log(data);
`,
"/a/dependency.js": /* js */ `
export default 1;
`,
"/a/hello/world/entry.js": /* js */ `
console.log(2);
`,
"/a/hello/world/a/a/a/a/a/a/a/entry.js": /* js */ `
console.log(3);
`,
},
entryNaming: "files/[dir]/file.[ext]",
entryPointsRaw: ["./a/hello/entry.js", "./a/hello/world/entry.js", "./a/hello/world/a/a/a/a/a/a/a/entry.js"],
run: [
{
file: "/out/files/file.js",
stdout: "1",
},
{
file: "/out/files/world/file.js",
stdout: "2",
},
{
file: "/out/files/world/a/a/a/a/a/a/a/file.js",
stdout: "3",
},
],
});
itBundled("naming/EntryNamingTemplate2", {
todo: true,
files: {
"/src/first.js": /* js */ `
console.log(1);
`,
"/src/second/third.js": /* js */ `
console.log(2);
`,
},
entryNaming: "[ext]/prefix[dir]suffix/file.[ext]",
entryPointsRaw: ["./src/first.js", "./src/second/third.js"],
run: [
{
file: "/out/js/prefix/secondsuffix/file.js",
stdout: "2",
},
{
file: "/out/js/prefix/suffix/file.js",
stdout: "1",
},
],
});
itBundled("naming/AssetNaming", {
files: {
"/src/lib/first/file.js": /* js */ `
import file from "../second/data.file";
console.log(file);
`,
"/src/lib/second/data.file": `
this is a file
`,
},
root: "/src",
entryNaming: "hello.[ext]",
assetNaming: "test.[ext]",
entryPointsRaw: ["./src/lib/first/file.js"],
run: {
file: "/out/hello.js",
stdout: "./test.file",
},
});
itBundled("naming/AssetNamingMkdir", {
files: {
"/src/lib/first/file.js": /* js */ `
import file from "../second/data.file";
console.log(file);
`,
"/src/lib/second/data.file": `
this is a file
`,
},
root: "/src",
entryNaming: "hello.[ext]",
assetNaming: "subdir/test.[ext]",
entryPointsRaw: ["./src/lib/first/file.js"],
run: {
file: "/out/hello.js",
stdout: "./subdir/test.file",
},
});
itBundled("naming/AssetNamingDir", {
files: {
"/src/lib/first/file.js": /* js */ `
import file from "../second/data.file";
console.log(file);
`,
"/src/lib/second/data.file": `
this is a file
`,
},
root: "/src",
entryNaming: "hello.[ext]",
assetNaming: "[dir]/test.[ext]",
entryPointsRaw: ["./src/lib/first/file.js"],
loader: ESBUILD
? {
".file": "file",
}
: undefined,
run: [
{
file: "/out/hello.js",
stdout: "./lib/second/test.file",
},
],
});
itBundled("naming/AssetNoOverwrite", {
todo: true,
files: {
"/src/entry.js": /* js */ `
import asset1 from "./asset1.file";
import asset2 from "./asset2.file";
console.log(asset1, asset2);
`,
"/src/asset1.file": `
file 1
`,
"/src/asset2.file": `
file 2
`,
},
root: "/src",
assetNaming: "same-filename.txt",
entryPointsRaw: ["./src/entry.js"],
loader: {
".file": "file",
},
bundleErrors: {
"<bun>": ['Multiple files share the same output path: "same-filename.txt"'],
},
});
itBundled("naming/AssetFileLoaderPath1", {
files: {
"/src/entry.js": /* js */ `
import asset1 from "./asset1.file";
console.log(asset1);
`,
"/src/asset1.file": `
file 1
`,
//
"/out/hello/_": "",
},
root: "/src",
entryNaming: "lib/entry.js",
assetNaming: "hello/same-filename.txt",
entryPointsRaw: ["./src/entry.js"],
loader: {
".file": "file",
},
});
itBundled("naming/NonexistantRoot", ({ root }) => ({
backend: "cli",
files: {
"/src/entry.js": /* js */ `
import asset1 from "./asset1.file";
console.log(asset1);
`,
"/src/asset1.file": `
file 1
`,
},
root: "/lib",
entryPointsRaw: ["./src/entry.js"],
bundleErrors: {
// "<bun>": [`FileNotFound: failed to open root directory: ${root}/lib`],
},
}));
itBundled("naming/EntrypointOutsideOfRoot", {
todo: true,
files: {
"/src/hello/entry.js": /* js */ `
console.log(1);
`,
"/src/root/file.js": /* js */ `
console.log(2);
`,
},
root: "/src/root",
entryPointsRaw: ["./src/hello/entry.js"],
run: {
file: "/out/_.._/hello/file.js",
},
});
itBundled("naming/WithPathTraversal", {
files: {
"/a/hello/entry.js": /* js */ `
import data from '../dependency'
console.log(data);
`,
"/a/dependency.js": /* js */ `
export default 1;
`,
"/a/hello/world/entry.js": /* js */ `
console.log(2);
`,
"/a/hello/world/a/a/a/a/a/a/a/entry.js": /* js */ `
console.log(3);
`,
},
entryNaming: "foo/../bar/[dir]/file.[ext]",
entryPointsRaw: ["./a/hello/entry.js", "./a/hello/world/entry.js", "./a/hello/world/a/a/a/a/a/a/a/entry.js"],
run: [
{
file: "/out/bar/file.js",
stdout: "1",
},
{
file: "/out/bar/world/file.js",
stdout: "2",
},
{
file: "/out/bar/world/a/a/a/a/a/a/a/file.js",
stdout: "3",
},
],
});
// A non-ASCII ID_Continue basename char is preserved in the generated
// CommonJS wrapper symbol, not replaced per-code-point (nor per-UTF-8-byte,
// which once regressed to `require_caf__utils`).
itBundled("naming/NonAsciiSourceFilenameSymbol", {
files: {
"/entry.js": /* js */ `
const u = require("./café-utils.js");
console.log(u.hi);
`,
"/café-utils.js": /* js */ `
module.exports = { hi: 1 };
`,
},
target: "bun",
onAfterBundle(api) {
// target: "bun" prints identifiers ASCII-only, so "é" is escaped.
api.expectFile("/out.js").toContain("require_caf\\u{e9}_utils");
api.expectFile("/out.js").not.toContain("require_caf__utils");
api.expectFile("/out.js").not.toContain("require_caf_utils");
},
run: { stdout: "1" },
});
// Unterminated `[` used to panic path_template_print; CLI backend isolates the crash.
for (const unterminated of ["[name", "[dir", "[ext", "[hash", "[target", "a[b.js", "[name]-[hash.js"]) {
itBundled(`naming/UnterminatedPlaceholder/${unterminated}`, {
backend: "cli",
files: {
"/src/entry.js": `console.log(1);`,
},
root: "/src",
entryNaming: unterminated,
entryPointsRaw: ["./src/entry.js"],
bundleErrors: {
"<bun>": [`--entry-naming: unterminated "[`],
},
});
}
for (const [opt, flag] of [
["chunkNaming", "--chunk-naming"],
["assetNaming", "--asset-naming"],
] as const) {
itBundled(`naming/UnterminatedPlaceholder/${flag}`, {
backend: "cli",
files: {
"/src/entry.js": `console.log(1);`,
},
root: "/src",
[opt]: "[name]-[hash",
entryPointsRaw: ["./src/entry.js"],
bundleErrors: {
"<bun>": [`${flag}: unterminated "[hash"`],
},
});
}
// An unknown `[placeholder]` is kept verbatim in the output path.
itBundled("naming/UnknownPlaceholderIsLiteral", {
backend: "cli",
files: {
"/src/entry.js": `console.log(1);`,
},
root: "/src",
entryNaming: "[nonexistent]-[name].[ext]",
entryPointsRaw: ["./src/entry.js"],
onAfterBundle(api) {
api.assertFileExists("/out/[nonexistent]-entry.js");
},
run: { file: "/out/[nonexistent]-entry.js", stdout: "1" },
});
});
// Bun.build({ naming: "[name" }) used to SIGABRT; validation now rejects it.
describe("bundler", () => {
for (const [naming, option] of [
[`"[name"`, "naming"],
[`{ entry: "[dir]/[name" }`, "naming.entry"],
[`{ chunk: "[name]-[hash" }`, "naming.chunk"],
[`{ asset: "pre[post" }`, "naming.asset"],
] as const) {
test(`naming/UnterminatedPlaceholderAPI ${naming}`, async () => {
using dir = tempDir("naming-unterminated", {
"e.ts": `console.log("hi");`,
"build.ts": `
try {
await Bun.build({ entrypoints: ["./e.ts"], outdir: "./out", throw: false, naming: ${naming} });
console.log("SURVIVED no-error");
} catch (e) {
console.log("SURVIVED", String(e));
}
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build.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).toContain(`${option}: unterminated "[`);
expect(stdout).toContain(`(missing "]")`);
expect(exitCode).toBe(0);
});
}
});