Files
bun-src/test/bundler/bundler_loader.test.ts
T
2026-08-27 21:09:14 +00:00

683 lines
22 KiB
TypeScript

import { fileURLToPath, Loader } from "bun";
import { describe, expect } from "bun:test";
import fs, { readdirSync } from "node:fs";
import { join } from "path";
import { itBundled } from "./expectBundled";
describe("bundler", async () => {
for (let target of ["bun", "node"] as const) {
describe(`${target} loader`, async () => {
itBundled("bun/loader-yaml-file", {
target,
files: {
"/entry.ts": /* js */ `
import hello from './hello.notyaml' with {type: "yaml"};
console.write(JSON.stringify(hello));
`,
"/hello.notyaml": `hello: world`,
},
run: { stdout: '{"hello":"world"}' },
});
itBundled("bun/loader-text-file", {
target,
outfile: "",
outdir: "/out",
files: {
"/entry.ts": /* js */ `
import hello from './hello.foo' with {type: "text"};
console.log(hello);
`,
"/hello.foo": "Hello, world!",
},
run: { stdout: "Hello, world!" },
});
itBundled("bun/loader-json-file", {
target,
files: {
"/entry.ts": /* js */ `
import hello from './hello.notjson' with {type: "json"};
console.write(JSON.stringify(hello));
`,
"/hello.notjson": JSON.stringify({ hello: "world" }),
},
run: { stdout: '{"hello":"world"}' },
});
itBundled("bun/loader-toml-file", {
target,
files: {
"/entry.ts": /* js */ `
import hello from './hello.nottoml' with {type: "toml"};
console.write(JSON.stringify(hello));
`,
"/hello.nottoml": `hello = "world"`,
},
run: { stdout: '{"hello":"world"}' },
});
// The Temporal reference is a real unbound symbol: a user binding named
// Temporal in the same bundle gets renamed instead of capturing the
// `Temporal.*.from` calls the TOML module compiles to.
itBundled("bun/loader-toml-datetime-shadowed-temporal-global", {
target,
files: {
"/entry.ts": /* js */ `
import cfg from './config.toml';
var Temporal = "shadowed";
console.write(Temporal + " " + cfg.ld.toString());
`,
"/config.toml": `ld = 1979-05-27`,
},
run: { stdout: "shadowed 1979-05-27" },
});
// The realistic collision: another module in the chunk imports a
// Temporal polyfill binding. The import gets renamed and the TOML
// module's calls still resolve to the native global.
itBundled("bun/loader-toml-datetime-imported-temporal-binding", {
target,
files: {
"/entry.ts": /* js */ `
import { Temporal } from './polyfill.js';
import cfg from './config.toml';
console.write(Temporal.tag + " " + (cfg.ld instanceof globalThis.Temporal.PlainDate) + " " + cfg.ld.toString());
`,
"/polyfill.js": `export const Temporal = { tag: "polyfill" };`,
"/config.toml": `ld = 1979-05-27`,
},
run: { stdout: "polyfill true 1979-05-27" },
});
itBundled("bun/loader-toml-datetime-no-bundle", {
target,
bundling: false,
entryPoints: ["/config.toml"],
files: {
"/config.toml": `d = 1979-05-27\n[t]\nat = 1979-05-27T00:32:00-07:00`,
},
run: true,
onAfterBundle(api) {
const code = api.readFile("/out.js");
expect(code).toContain('Temporal.PlainDate.from("1979-05-27")');
expect(code).toContain('Temporal.Instant.from("1979-05-27T00:32:00-07:00")');
},
});
// TOML date/time values bundle as Temporal construction calls; the
// bundled module yields the same values Bun.TOML.parse returns.
itBundled("bun/loader-toml-datetime", {
target,
files: {
"/entry.ts": /* js */ `
import cfg, { lt } from './config.toml';
console.write(JSON.stringify([
cfg.odt instanceof Temporal.Instant, cfg.odt.toString(),
cfg.ldt instanceof Temporal.PlainDateTime, cfg.ldt.toString(),
cfg.ld instanceof Temporal.PlainDate, cfg.ld.toString(),
lt instanceof Temporal.PlainTime, lt.toString(),
cfg.tbl.arr[0].toString(),
]));
`,
"/config.toml": `odt = 1979-05-27T00:32:00-07:00\nldt = 1979-05-27 07:32\nld = 1979-05-27\nlt = 07:32:00.500\n[tbl]\narr = [ 07:32:00 ]`,
},
run: {
stdout:
'[true,"1979-05-27T07:32:00Z",true,"1979-05-27T07:32:00",true,"1979-05-27",true,"07:32:00.5","07:32:00"]',
},
});
itBundled("bun/loader-text-file", {
target,
files: {
"/entry.ts": /* js */ `
import hello from './hello.json' with {type: "text"};
console.write(hello);
`,
"/hello.json": JSON.stringify({ hello: "world" }),
},
run: { stdout: '{"hello":"world"}' },
});
itBundled("bun/loader-xml-file", {
target,
files: {
"/entry.ts": /* js */ `
import doc from './hello.notxml' with {type: "xml"};
import byExtension, { greeting } from './hello.xml';
console.write(JSON.stringify([doc, byExtension, greeting]));
`,
"/hello.notxml": `<hello to="world">hi <b>there</b></hello>`,
"/hello.xml": `<?xml version="1.0"?><!DOCTYPE greeting [<!ENTITY w "world">]><greeting __proto__="1"><to>&w;</to><to>you</to></greeting>`,
},
run: {
stdout:
'[{"hello":{"@to":"world","#text":"hi ","b":"there"}},{"greeting":{"@__proto__":"1","to":["world","you"]}},{"@__proto__":"1","to":["world","you"]}]',
},
});
});
}
itBundled("bun/loader-text-file", {
target: "bun",
outfile: "",
outdir: "/out",
files: {
"/entry.ts": /* js */ `
import first from './1.boo' with {type: "text"};
import second from './2.boo' with {type: "text"};
console.write(first + second);
`,
"/1.boo": "'`Hello, \nworld!`",
"/2.boo": "`${Hello}\n, world!`'",
},
run: {
stdout: "'`Hello, \nworld!``${Hello}\n, world!`'",
},
});
itBundled("bun/loader-json-proto-key-is-own-property", {
target: "bun",
files: {
"/entry.ts": /* js */ `
import data from './data.json';
const out = [
Object.getPrototypeOf(data) === Object.prototype,
Object.hasOwn(data, "__proto__"),
data.x,
JSON.stringify(data),
];
console.write(JSON.stringify(out));
`,
"/data.json": `{"__proto__": {"x": 1}, "a": 2}`,
},
run: { stdout: '[true,true,null,"{\\"__proto__\\":{\\"x\\":1},\\"a\\":2}"]' },
});
itBundled("bun/loader-toml-proto-key-is-own-property", {
target: "bun",
files: {
"/entry.ts": /* js */ `
import data from './data.toml';
const out = [
Object.getPrototypeOf(data) === Object.prototype,
Object.hasOwn(data, "__proto__"),
data.x,
JSON.stringify(data),
];
console.write(JSON.stringify(out));
`,
"/data.toml": `a = 2\n[__proto__]\nx = 1\n`,
},
run: { stdout: '[true,true,null,"{\\"a\\":2,\\"__proto__\\":{\\"x\\":1}}"]' },
});
itBundled("bun/loader-yaml-proto-key-is-own-property", {
target: "bun",
files: {
"/entry.ts": /* js */ `
import data from './data.yaml';
const out = [
Object.getPrototypeOf(data) === Object.prototype,
Object.hasOwn(data, "__proto__"),
data.x,
JSON.stringify(data),
];
console.write(JSON.stringify(out));
`,
"/data.yaml": `__proto__:\n x: 1\na: 2\n`,
},
run: { stdout: '[true,true,null,"{\\"__proto__\\":{\\"x\\":1},\\"a\\":2}"]' },
});
itBundled("bun/loader-jsonc-proto-key-is-own-property", {
target: "bun",
files: {
"/entry.ts": /* js */ `
import data from './data.jsonc';
const out = [
Object.getPrototypeOf(data) === Object.prototype,
Object.hasOwn(data, "__proto__"),
data.x,
JSON.stringify(data),
];
console.write(JSON.stringify(out));
`,
"/data.jsonc": `// jsonc\n{"__proto__": {"x": 1}, "a": 2,}`,
},
run: { stdout: '[true,true,null,"{\\"__proto__\\":{\\"x\\":1},\\"a\\":2}"]' },
});
itBundled("bun/loader-json5-proto-key-is-own-property", {
target: "bun",
files: {
"/entry.ts": /* js */ `
import data from './data.json5';
const out = [
Object.getPrototypeOf(data) === Object.prototype,
Object.hasOwn(data, "__proto__"),
data.x,
JSON.stringify(data),
];
console.write(JSON.stringify(out));
`,
"/data.json5": `{__proto__: {x: 1}, a: 2}`,
},
run: { stdout: '[true,true,null,"{\\"__proto__\\":{\\"x\\":1},\\"a\\":2}"]' },
});
itBundled("bun/loader-json-nested-proto-key-is-own-property", {
target: "bun",
files: {
"/entry.ts": /* js */ `
import data from './data.json';
const nested = data.nested;
const out = [
Object.getPrototypeOf(nested) === Object.prototype,
Object.hasOwn(nested, "__proto__"),
nested.x,
JSON.stringify(data),
];
console.write(JSON.stringify(out));
`,
"/data.json": `{"nested": {"__proto__": {"x": 1}, "a": 2}}`,
},
run: { stdout: '[true,true,null,"{\\"nested\\":{\\"__proto__\\":{\\"x\\":1},\\"a\\":2}}"]' },
});
itBundled("bun/loader-toml-inline-table-proto-key-is-own-property", {
target: "bun",
files: {
"/entry.ts": /* js */ `
import data from './data.toml';
const out = [
Object.getPrototypeOf(data) === Object.prototype,
Object.hasOwn(data, "__proto__"),
data.x,
JSON.stringify(data),
];
console.write(JSON.stringify(out));
`,
"/data.toml": `a = 2\n"__proto__" = { x = 1 }\n`,
},
run: { stdout: '[true,true,null,"{\\"a\\":2,\\"__proto__\\":{\\"x\\":1}}"]' },
});
itBundled("bun/loader-yaml-flow-proto-key-is-own-property", {
target: "bun",
files: {
"/entry.ts": /* js */ `
import data from './data.yaml';
const out = [
Object.getPrototypeOf(data) === Object.prototype,
Object.hasOwn(data, "__proto__"),
data.x,
JSON.stringify(data),
];
console.write(JSON.stringify(out));
`,
"/data.yaml": `{__proto__: {x: 1}, a: 2}\n`,
},
run: { stdout: '[true,true,null,"{\\"__proto__\\":{\\"x\\":1},\\"a\\":2}"]' },
});
itBundled("bun/loader-xml-proto-key-is-own-property", {
target: "bun",
files: {
"/entry.ts": /* js */ `
import data from './data.xml';
const out = [
Object.getPrototypeOf(data.r) === Object.prototype,
Object.hasOwn(data.r, "__proto__"),
data.r.x,
JSON.stringify(data),
];
console.write(JSON.stringify(out));
`,
"/data.xml": `<r><__proto__><x>1</x></__proto__><a>2</a></r>`,
},
run: { stdout: '[true,true,null,"{\\"r\\":{\\"__proto__\\":{\\"x\\":\\"1\\"},\\"a\\":\\"2\\"}}"]' },
});
itBundled("bun/loader-xml-entry-point", {
target: "bun",
outfile: "",
outdir: "/out",
files: {
"/feed.xml": `<?xml version="1.0"?><feed><entry id="1">one</entry><entry id="2">two</entry></feed>`,
},
entryPoints: ["/feed.xml"],
entryNaming: "[dir]/[name]-[hash].[ext]",
onAfterBundle(api) {
const jsFile = readdirSync(api.outdir).find(x => x.endsWith(".js"))!;
const module = require(join(api.outdir, jsFile));
expect(module.default).toStrictEqual({
feed: {
entry: [
{ "@id": "1", "#text": "one" },
{ "@id": "2", "#text": "two" },
],
},
});
},
});
itBundled("bun/loader-xml-syntax-error", {
target: "bun",
files: {
"/entry.ts": /* js */ `
import data from './bad.xml';
console.log(data);
`,
"/bad.xml": `<config>\n <port>8080</bad>\n</config>`,
},
bundleErrors: {
"/bad.xml": ["Expected closing tag </port> but found </bad>"],
},
});
// The CSS-modules lazy export builds its object through `E::Object::put`.
itBundled("bun/loader-css-module-proto-class-is-own-property", {
target: "bun",
outdir: "/out",
files: {
"/entry.ts": /* js */ `
import styles from './styles.module.css';
const out = [
Object.getPrototypeOf(styles) === Object.prototype,
Object.hasOwn(styles, "__proto__"),
typeof styles.a === "string",
];
console.write(JSON.stringify(out));
`,
"/styles.module.css": `.__proto__ { color: red; }\n.a { color: blue; }\n`,
},
run: { stdout: "[true,true,true]" },
});
itBundled("bun/wasm-is-copied-to-outdir", {
target: "bun",
outdir: "/out",
files: {
"/entry.ts": /* js */ `
import wasm from './add.wasm';
import { join } from 'path';
const { instance } = await WebAssembly.instantiate(await Bun.file(join(import.meta.dir, wasm)).arrayBuffer());
console.log(instance.exports.add(1, 2));
`,
"/add.wasm": fs.readFileSync(join(import.meta.dir, "fixtures", "add.wasm")),
},
run: {
stdout: "3",
},
});
const moon = await Bun.file(
fileURLToPath(import.meta.resolve("../js/bun/util/text-loader-fixture-text-file.backslashes.txt")),
).text();
// https://github.com/oven-sh/bun/issues/3449
itBundled("bun/loader-text-file-#3449", {
target: "bun",
outfile: "",
outdir: "/out",
files: {
"/entry.ts": /* js */ `
import first from './1.boo' with {type: "text"};
console.write(first);
`,
"/1.boo": moon,
},
run: {
stdout: moon,
},
});
const loaders: Loader[] = ["wasm", "json", "file" /* "napi" */, "text"];
const exts = ["wasm", "json", "lmao" /* ".node" */, "txt"];
for (let i = 0; i < loaders.length; i++) {
const loader = loaders[i];
const ext = exts[i];
itBundled(`bun/loader-copy-file-entry-point-with-onLoad-${loader}`, {
target: "bun",
outdir: "/out",
files: {
[`/entry.${ext}`]: /* js */ `{ "hello": "friends" }`,
},
entryNaming: "[dir]/[name]-[hash].[ext]",
plugins(builder) {
builder.onLoad({ filter: new RegExp(`.${loader}$`) }, async ({ path }) => {
const result = await Bun.file(path).text();
return { contents: result, loader };
});
},
onAfterBundle(api) {
const jsFile = readdirSync(api.outdir).find(x => x.endsWith(".js"))!;
const module = require(join(api.outdir, jsFile));
if (loader === "json") {
expect(module.default).toStrictEqual({ hello: "friends" });
} else if (loader === "text") {
expect(module.default).toStrictEqual('{ "hello": "friends" }');
} else {
api.assertFileExists(join("out", module.default));
}
},
});
}
for (let i = 0; i < loaders.length; i++) {
const loader = loaders[i];
const ext = exts[i];
itBundled(`bun/loader-copy-file-entry-point-${loader}`, {
target: "bun",
outfile: "",
outdir: "/out",
files: {
[`/entry.${ext}`]: /* js */ `{ "hello": "friends" }`,
},
entryNaming: "[dir]/[name]-[hash].[ext]",
onAfterBundle(api) {
const jsFile = readdirSync(api.outdir).find(x => x.endsWith(".js"))!;
const module = require(join(api.outdir, jsFile));
if (loader === "json") {
expect(module.default).toStrictEqual({ hello: "friends" });
} else if (loader === "text") {
expect(module.default).toStrictEqual('{ "hello": "friends" }');
} else {
api.assertFileExists(join("out", module.default));
}
},
});
}
describe("handles empty files", () => {
for (const target of ["bun", "node", "browser"] as const) {
itBundled(`${target}/loader-empty-text-file`, {
target: target,
files: {
"/entry.ts": /* js */ `
import empty from './empty.txt' with {type: "text"};
console.write(JSON.stringify(empty));
`,
"/empty.txt": "",
},
run: { stdout: '""' },
});
itBundled(`${target}/loader-empty-file-loader`, {
target: target,
outdir: "/out",
files: {
"/entry.ts": /* js */ `
import empty from './empty.txt' with {type: "file"};
export default empty;
`,
"/empty.txt": "",
},
onAfterBundle(api) {
const jsFile = readdirSync(api.outdir).find(x => x.endsWith(".js"))!;
const module = require(join(api.outdir, jsFile));
api.assertFileExists(join("out", module.default));
},
});
}
});
// Lazy-export modules (JSON, TOML, CSS modules, ...) used to crash the
// printer when bundled with the dev server's module format.
// https://github.com/oven-sh/bun/issues/31943
describe("internal_bake_dev lazy exports", () => {
itBundled("bake-dev/loader-json-default-import", {
format: "internal_bake_dev",
files: {
"/entry.ts": /* js */ `
import data from "./data.json";
console.log(data.value);
`,
"/data.json": `{"value": 1}`,
},
onAfterBundle(api) {
const output = api.readFile("/out.js");
expect(output).toContain('"data.json"(hmr, module, exports) {');
expect(output).toContain("module.exports = { value: 1 }");
expect(output).toContain("import_data.default.value");
},
});
itBundled("bake-dev/loader-json-named-and-star-import", {
format: "internal_bake_dev",
files: {
"/entry.ts": /* js */ `
import { value } from "./data.json";
import * as ns from "./data.json";
console.log(value, ns.value);
`,
"/data.json": `{"value": 1}`,
},
onAfterBundle(api) {
const output = api.readFile("/out.js");
expect(output).toContain('"data.json"(hmr, module, exports) {');
expect(output).toContain("module.exports = { value: 1 }");
},
});
itBundled("bake-dev/loader-json-require", {
format: "internal_bake_dev",
files: {
"/entry.ts": /* js */ `
const data = require("./data.json");
console.log(data.value);
`,
"/data.json": `{"value": 1}`,
},
onAfterBundle(api) {
const output = api.readFile("/out.js");
expect(output).toContain('"data.json"(hmr, module, exports) {');
expect(output).toContain("module.exports = { value: 1 }");
},
});
itBundled("bake-dev/loader-json-entry-point", {
format: "internal_bake_dev",
files: {
"/data.json": `{"value": 1}`,
},
entryPoints: ["/data.json"],
onAfterBundle(api) {
const output = api.readFile("/out.js");
expect(output).toContain('"data.json"(hmr, module, exports) {');
expect(output).toContain("module.exports = { value: 1 }");
},
});
itBundled("bake-dev/loader-jsonc-default-import", {
format: "internal_bake_dev",
files: {
"/entry.ts": /* js */ `
import data from "./data.jsonc";
console.log(data.value);
`,
"/data.jsonc": `{
// comment
"value": 1,
}`,
},
onAfterBundle(api) {
const output = api.readFile("/out.js");
expect(output).toContain('"data.jsonc"(hmr, module, exports) {');
expect(output).toContain("module.exports = {");
expect(output).toContain("value: 1");
},
});
itBundled("bake-dev/loader-toml-default-import", {
format: "internal_bake_dev",
files: {
"/entry.ts": /* js */ `
import data from "./data.toml";
console.log(data.value);
`,
"/data.toml": `value = 1`,
},
onAfterBundle(api) {
const output = api.readFile("/out.js");
expect(output).toContain('"data.toml"(hmr, module, exports) {');
expect(output).toContain("module.exports = {");
expect(output).toContain("value: 1");
expect(output).toContain("import_data.default.value");
},
});
itBundled("bake-dev/loader-empty-cjs-import", {
format: "internal_bake_dev",
files: {
"/entry.ts": /* js */ `
import x from "./empty.cjs";
console.log(x);
`,
"/empty.cjs": "",
},
onAfterBundle(api) {
const output = api.readFile("/out.js");
expect(output).toContain('"empty.cjs"(hmr, module, exports) {');
expect(output).toContain("module.exports = {}");
},
});
itBundled("bake-dev/loader-empty-mjs-import", {
format: "internal_bake_dev",
files: {
"/entry.ts": /* js */ `
import x from "./empty.mjs";
console.log(x);
`,
"/empty.mjs": "",
},
onAfterBundle(api) {
const output = api.readFile("/out.js");
expect(output).toContain('"empty.mjs"(hmr, module, exports) {');
expect(output).toContain("module.exports = undefined");
},
});
// CSS imports are delivered out-of-band by the dev server, so the JS
// chunk only contains the importing module. This used to panic while
// linking the CSS file's lazy-export JS stub.
itBundled("bake-dev/loader-css-module-import", {
format: "internal_bake_dev",
outdir: "/out",
files: {
"/entry.ts": /* js */ `
import styles from "./styles.module.css";
console.log(styles.foo);
`,
"/styles.module.css": `.foo { color: red; }`,
},
onAfterBundle(api) {
const jsFile = readdirSync(api.outdir).find(x => x.endsWith(".js"))!;
expect(api.readFile(join("/out", jsFile))).toContain('"entry.ts"');
const cssFile = readdirSync(api.outdir).find(x => x.endsWith(".css"))!;
expect(api.readFile(join("/out", cssFile))).toContain("color: red");
},
});
});
});