Files
bun.sh/test/bake/dev/bundle.test.ts

592 lines
17 KiB
TypeScript

// Bundle tests are tests concerning bundling bugs that only occur in DevServer.
import { expect } from "bun:test";
import { devTest, emptyHtmlFile, minimalFramework } from "../bake-harness";
devTest("import identifier doesnt get renamed", {
framework: minimalFramework,
files: {
"db.ts": `export const abc = "123";`,
"routes/index.ts": `
import { abc } from '../db';
export default function (req, meta) {
let v1 = "";
const v2 = v1
? abc.toFixed(2)
: abc.toString();
return new Response('Hello, ' + v2 + '!');
}
`,
},
async test(dev) {
await dev.fetch("/").equals("Hello, 123!");
await dev.write("db.ts", `export const abc = "456";`);
await dev.fetch("/").equals("Hello, 456!");
await dev.patch("routes/index.ts", {
find: "Hello",
replace: "Bun",
});
await dev.fetch("/").equals("Bun, 456!");
},
});
devTest("symbol collision with import identifier", {
framework: minimalFramework,
files: {
"db.ts": `export const abc = "123";`,
"routes/index.ts": `
let import_db = 987;
import { abc } from '../db';
export default function (req, meta) {
let v1 = "";
const v2 = v1
? abc.toFixed(2)
: abc.toString();
return new Response('Hello, ' + v2 + ', ' + import_db + '!');
}
`,
},
async test(dev) {
await dev.fetch("/").equals("Hello, 123, 987!");
await dev.write("db.ts", `export const abc = "456";`);
await dev.fetch("/").equals("Hello, 456, 987!");
},
});
devTest('uses "development" condition', {
framework: minimalFramework,
files: {
"node_modules/example/package.json": JSON.stringify({
name: "example",
version: "1.0.0",
exports: {
".": {
development: "./development.js",
default: "./production.js",
},
},
}),
"node_modules/example/development.js": `export default "development";`,
"node_modules/example/production.js": `export default "production";`,
"routes/index.ts": `
import environment from 'example';
export default function (req, meta) {
return new Response('Environment: ' + environment);
}
`,
},
async test(dev) {
await dev.fetch("/").equals("Environment: development");
},
});
devTest("importing a file before it is created", {
files: {
"index.html": emptyHtmlFile({
styles: [],
scripts: ["index.ts"],
}),
"index.ts": `
import { abc } from './second';
console.log('value: ' + abc);
`,
},
async test(dev) {
await using c = await dev.client("/", {
errors: [`index.ts:1:21: error: Could not resolve: "./second"`],
});
await c.expectReload(async () => {
await dev.write("second.ts", `export const abc = "456";`);
});
await c.expectMessage("value: 456");
},
});
devTest("default export same-scope handling", {
files: {
"index.html": emptyHtmlFile({
styles: [],
scripts: ["index.ts"],
}),
"index.ts": `
import.meta.hot.accept();
await import("./fixture1.ts");
console.log((new ((await import("./fixture2.ts")).default)).a);
await import("./fixture3.ts");
console.log((new ((await import("./fixture4.ts")).default)).result);
console.log((await import("./fixture5.ts")).default);
console.log((await import("./fixture6.ts")).default);
console.log((await import("./fixture7.ts")).default());
console.log((await import("./fixture8.ts")).default());
console.log((await import("./fixture9.ts")).default(false));
`,
"fixture1.ts": `
const sideEffect = () => "a";
export default class A {
[sideEffect()] = "ONE";
}
console.log(new A().a);
`,
"fixture2.ts": `
const sideEffect = () => "a";
export default class A {
[sideEffect()] = "TWO";
}
`,
"fixture3.ts": `
export default class A {
result = "THREE"
}
console.log(new A().result);
`,
"fixture4.ts": `
import.meta.hot.accept();
export default class MOVE {
result = "FOUR"
}
`,
"fixture5.ts": `
const default_export = "FIVE";
export default default_export;
`,
"fixture6.ts": `
const default_export = "S";
function sideEffect() {
return default_export + "EVEN";
}
export default sideEffect();
console.log(default_export + "IX");
`,
"fixture7.ts": `
export default function() { return "EIGHT" };
`,
"fixture8.ts": `
import.meta.hot.accept();
export default function MOVE() { return "NINE" };
`,
"fixture9.ts": `
export default function named(flag = true) { return flag ? "TEN" : "ELEVEN" };
console.log(named());
`,
},
async test(dev) {
await using c = await dev.client("/", { storeHotChunks: true });
c.expectMessage(
//
"ONE",
"TWO",
"THREE",
"FOUR",
"FIVE",
"SIX",
"SEVEN",
"EIGHT",
"NINE",
"TEN",
"ELEVEN",
);
const filesExpectingMove = Object.entries(dev.options.files)
.filter(([, content]) => content.includes("MOVE"))
.map(([path]) => path);
for (const file of filesExpectingMove) {
await dev.writeNoChanges(file);
const chunk = await c.getMostRecentHmrChunk();
expect(chunk).toMatch(/default:\s*(function|class)\s*MOVE/);
}
await dev.writeNoChanges("fixture7.ts");
const chunk = await c.getMostRecentHmrChunk();
expect(chunk).toMatch(/default:\s*function/);
// Since fixture7.ts is not marked as accepting, it will bubble the update
// to `index.ts`, re-evaluate it and some of the dependencies.
c.expectMessage("TWO", "FOUR", "FIVE", "SEVEN", "EIGHT", "NINE", "ELEVEN");
},
});
devTest("directory cache bust case #17576", {
files: {
"web/index.html": emptyHtmlFile({
styles: [],
scripts: ["index.ts"],
}),
"web/index.ts": `
console.log(123);
import.meta.hot.accept();
`,
},
mainDir: "server",
async test(dev) {
await using c = await dev.client("/");
await c.expectMessage(123);
await c.expectNoWebSocketActivity(async () => {
await dev.write(
"web/Test.ts",
`
export const abc = 456;
`,
);
});
await dev.write(
"web/index.ts",
`
import { abc } from "./Test.ts";
console.log(abc);
`,
);
await c.expectMessage(456);
},
});
devTest("deleting imported file shows error then recovers", {
skip: [
"win32", // unlinkSync is having weird behavior
],
files: {
"index.html": emptyHtmlFile({
styles: [],
scripts: ["index.ts"],
}),
"index.ts": `
import { value } from "./other";
console.log(value);
`,
"other.ts": `
export const value = 123;
`,
"unrelated.ts": `
export const value = 123;
`,
},
async test(dev) {
await using c = await dev.client("/");
await c.expectMessage(123);
await dev.delete("other.ts", {
errors: ['index.ts:1:23: error: Could not resolve: "./other"'],
});
await c.expectReload(async () => {
await dev.write(
"other.ts",
`
export const value = 456;
`,
);
});
await c.expectMessage(456);
await c.expectNoWebSocketActivity(async () => {
await dev.delete("unrelated.ts");
});
},
});
devTest("importing html file", {
files: {
"index.html": emptyHtmlFile({
styles: [],
scripts: ["index.ts"],
}),
"index.ts": `
import html from "./index.html";
console.log(html);
`,
},
async test(dev) {
await using c = await dev.client("/", {
errors: ["index.ts:1:18: error: Browser builds cannot import HTML files."],
});
},
});
devTest("importing html file with text loader (#18154)", {
files: {
"index.html": emptyHtmlFile({
styles: [],
scripts: ["index.ts"],
}),
"index.ts": `
import html from "./app.html" with { type: "text" };
console.log(html);
`,
"app.html": "<div>hello world</div>",
},
htmlFiles: ["index.html"],
async test(dev) {
await using c = await dev.client("/", {});
await c.expectMessage("<div>hello world</div>");
},
});
devTest("importing bun on the client", {
files: {
"index.html": emptyHtmlFile({
styles: [],
scripts: ["index.ts"],
}),
"index.ts": `
import bun from "bun";
console.log(bun);
`,
},
async test(dev) {
await using c = await dev.client("/", {
errors: ['index.ts:1:17: error: Browser build cannot import Bun builtin: "bun"'],
});
},
});
devTest("import.meta.main", {
files: {
"index.html": emptyHtmlFile({
styles: [],
scripts: ["index.ts"],
}),
"index.ts": `
console.log(import.meta.main);
import.meta.hot.accept();
`,
},
async test(dev) {
await using c = await dev.client("/");
await c.expectMessage(false); // import.meta.main is always false because there is no single entry point
await dev.write(
"index.ts",
`
require;
console.log(import.meta.main);
`,
);
await c.expectMessage(false);
},
});
devTest("commonjs forms", {
timeoutMultiplier: 2,
files: {
"index.html": emptyHtmlFile({
styles: [],
scripts: ["index.ts"],
}),
"index.ts": `
import cjs from "./cjs.js";
console.log(cjs);
`,
"cjs.js": `
module.exports.field = {};
`,
},
async test(dev) {
console.log("Initial");
await using c = await dev.client("/");
console.log(" expecting message");
await c.expectMessage({ field: {} });
console.log(" expecting reload");
await c.expectReload(async () => {
console.log(" writing");
await dev.write("cjs.js", `exports.field = "1";`);
console.log(" now reloading");
});
console.log(" expecting message");
await c.expectMessage({ field: "1" });
console.log("Second");
console.log(" expecting reload");
await c.expectReload(async () => {
console.log(" writing");
await dev.write("cjs.js", `let theExports = exports; theExports.field = "2";`);
});
console.log(" expecting message");
await c.expectMessage({ field: "2" });
console.log("Third");
console.log(" expecting reload");
await c.expectReload(async () => {
console.log(" writing");
await dev.write("cjs.js", `let theModule = module; theModule.exports.field = "3";`);
});
console.log(" expecting message");
await c.expectMessage({ field: "3" });
console.log("Fourth");
await c.expectReload(async () => {
await dev.write("cjs.js", `let { exports } = module; exports.field = "4";`);
});
await c.expectMessage({ field: "4" });
console.log("Fifth");
await c.expectReload(async () => {
await dev.write("cjs.js", `var { exports } = module; exports.field = "4.5";`);
});
await c.expectMessage({ field: "4.5" });
console.log("Sixth");
await c.expectReload(async () => {
await dev.write("cjs.js", `let theExports = module.exports; theExports.field = "5";`);
});
await c.expectMessage({ field: "5" });
console.log("Seventh");
await c.expectReload(async () => {
await dev.write("cjs.js", `require; eval("module.exports.field = '6'");`);
});
await c.expectMessage({ field: "6" });
},
});
// --- Barrel optimization tests ---
devTest("barrel optimization skips unused submodules", {
files: {
"index.html": emptyHtmlFile({ scripts: ["index.ts"] }),
"index.ts": `
import { Alpha } from 'barrel-lib';
console.log('got: ' + Alpha);
`,
"node_modules/barrel-lib/package.json": JSON.stringify({
name: "barrel-lib",
version: "1.0.0",
main: "./index.js",
sideEffects: false,
}),
"node_modules/barrel-lib/index.js": `
export { Alpha } from './alpha.js';
export { Beta } from './beta.js';
export { Gamma } from './gamma.js';
`,
"node_modules/barrel-lib/alpha.js": `export const Alpha = "ALPHA";`,
"node_modules/barrel-lib/beta.js": `export const Beta = <<<SYNTAX_ERROR>>>;`,
"node_modules/barrel-lib/gamma.js": `export const Gamma = <<<SYNTAX_ERROR>>>;`,
},
async test(dev) {
// Beta.js and Gamma.js have syntax errors.
// If barrel optimization works, they are never parsed, so no error.
await using c = await dev.client("/");
await c.expectMessage("got: ALPHA");
},
});
devTest("barrel optimization: adding a new import triggers reload", {
files: {
"index.html": emptyHtmlFile({ scripts: ["index.ts"] }),
"index.ts": `
import { Alpha } from 'barrel-lib';
console.log('result: ' + Alpha);
`,
"node_modules/barrel-lib/package.json": JSON.stringify({
name: "barrel-lib",
version: "1.0.0",
main: "./index.js",
sideEffects: false,
}),
"node_modules/barrel-lib/index.js": `
export { Alpha } from './alpha.js';
export { Beta } from './beta.js';
export { Gamma } from './gamma.js';
`,
"node_modules/barrel-lib/alpha.js": `export const Alpha = "ALPHA";`,
"node_modules/barrel-lib/beta.js": `export const Beta = "BETA";`,
"node_modules/barrel-lib/gamma.js": `export const Gamma = "GAMMA";`,
},
async test(dev) {
await using c = await dev.client("/");
await c.expectMessage("result: ALPHA");
// Add a second import from the barrel — Beta was previously deferred,
// now needs to be loaded. The barrel file should be re-bundled with
// Beta un-deferred.
await c.expectReload(async () => {
await dev.write(
"index.ts",
`
import { Alpha, Beta } from 'barrel-lib';
console.log('result: ' + Alpha + ' ' + Beta);
`,
);
});
await c.expectMessage("result: ALPHA BETA");
// Add a third import
await c.expectReload(async () => {
await dev.write(
"index.ts",
`
import { Alpha, Beta, Gamma } from 'barrel-lib';
console.log('result: ' + Alpha + ' ' + Beta + ' ' + Gamma);
`,
);
});
await c.expectMessage("result: ALPHA BETA GAMMA");
},
});
devTest("barrel optimization: multi-file imports preserved across rebuilds", {
files: {
"index.html": emptyHtmlFile({ scripts: ["index.ts"] }),
"index.ts": `
import { Alpha } from 'barrel-lib';
import { value } from './other';
console.log('result: ' + Alpha + ' ' + value);
`,
"other.ts": `
import { Beta } from 'barrel-lib';
export const value = Beta;
`,
"node_modules/barrel-lib/package.json": JSON.stringify({
name: "barrel-lib",
version: "1.0.0",
main: "./index.js",
sideEffects: false,
}),
"node_modules/barrel-lib/index.js": `
export { Alpha } from './alpha.js';
export { Beta } from './beta.js';
export { Gamma } from './gamma.js';
`,
"node_modules/barrel-lib/alpha.js": `export const Alpha = "ALPHA";`,
"node_modules/barrel-lib/beta.js": `export const Beta = "BETA";`,
"node_modules/barrel-lib/gamma.js": `export const Gamma = "GAMMA";`,
},
async test(dev) {
await using c = await dev.client("/");
await c.expectMessage("result: ALPHA BETA");
// Edit only other.ts to also import Gamma. Alpha (from index.ts) must
// still be available even though index.ts is not re-parsed.
await c.expectReload(async () => {
await dev.write(
"other.ts",
`
import { Beta, Gamma } from 'barrel-lib';
export const value = Beta + ' ' + Gamma;
`,
);
});
await c.expectMessage("result: ALPHA BETA GAMMA");
},
});
devTest("barrel optimization: two export-from blocks pointing to the same source", {
files: {
"index.html": emptyHtmlFile({ scripts: ["index.ts"] }),
"index.ts": `
import { invariant } from 'barrel-lib';
console.log('got: ' + typeof invariant);
`,
"node_modules/barrel-lib/package.json": JSON.stringify({
name: "barrel-lib",
version: "1.0.0",
main: "./index.js",
sideEffects: false,
}),
"node_modules/barrel-lib/index.js": `
export {
createDataProperty,
defineProperty,
} from './utils.js';
export { unrelated } from './other.js';
export {
invariant,
} from './utils.js';
`,
"node_modules/barrel-lib/utils.js": `
export function createDataProperty() {}
export function defineProperty() {}
export function invariant(cond, msg) {
if (!cond) throw new Error(msg);
}
`,
"node_modules/barrel-lib/other.js": `export const unrelated = "OTHER";`,
},
async test(dev) {
await using c = await dev.client("/");
await c.expectMessage("got: function");
},
});