Files
bun.sh/test/bake/dev/hot.test.ts
2025-03-14 21:24:14 -07:00

473 lines
13 KiB
TypeScript

// Hot tests ensure that the `import.meta.hot` interface is functional
import { expect } from "bun:test";
import { devTest, emptyHtmlFile } from "../bake-harness";
devTest("import.meta.hot.accept basic", {
files: {
"index.html": emptyHtmlFile({
scripts: ["index.ts"],
}),
"index.ts": `
console.log("Hello, world!");
`,
},
async test(dev) {
await using c = await dev.client("/");
await c.expectMessage("Hello, world!");
await c.expectReload(async () => {
await dev.write(
"index.ts",
`
console.log("Hello, Bun!");
import.meta.hot.accept(newModule => {
console.log(Object.keys(newModule));
console.log(newModule.method());
});
`,
);
});
await c.expectMessage("Hello, Bun!");
await dev.write(
"index.ts",
`
export function method() {
return "Bun";
}
import.meta.hot.accept(newModule => {
console.log(Object.keys(newModule));
});
`,
);
await c.expectMessage(["method"], "Bun");
await dev.write(
"index.ts",
`
console.log("Without anything.");
`,
);
await c.expectMessage("Without anything.", []);
await c.expectReload(async () => {
await dev.writeNoChanges("index.ts");
});
await c.expectMessage("Without anything.");
},
});
devTest("import.meta.hot.accept patches imports", {
files: {
"index.html": emptyHtmlFile({
scripts: ["a.ts"],
}),
"a.ts": `
import { doSomething } from './b';
console.log("A");
globalThis.callFunction = () => doSomething();
`,
"b.ts": `
import { reasonableState, inc } from './c';
console.log("B");
let b = 0;
export function doSomething() {
using _ = { [Symbol.dispose]: inc };
return "A!" + (b++) + "!" + (reasonableState);
}
import.meta.hot.accept();
`,
"c.ts": `
export let reasonableState = 0;
export function inc() {
reasonableState++;
}
console.log("C");
// import.meta.hot.accept();
`,
},
async test(dev) {
await using c = await dev.client("/");
await c.expectMessage("C", "B", "A");
expect(await c.js<string>`callFunction()`).toBe("A!0!0");
expect(await c.js<string>`callFunction()`).toBe("A!1!1");
await dev.patch("c.ts", { find: "0", replace: "5" });
await c.expectMessage("C", "B"); // C does not self-accept
expect(await c.js<string>`callFunction()`).toBe("A!0!5");
expect(await c.js<string>`callFunction()`).toBe("A!1!6");
await dev.patch("b.ts", { find: "A!", replace: "B!" });
await c.expectMessage("B"); // B does not cause C to re-evaluate
expect(await c.js<string>`callFunction()`).toBe("B!0!7");
expect(await c.js<string>`callFunction()`).toBe("B!1!8");
await dev.patch("c.ts", { find: "// ", replace: "" });
await c.expectMessage("C", "B"); // C does not self-accept YET
expect(await c.js<string>`callFunction()`).toBe("B!0!5");
expect(await c.js<string>`callFunction()`).toBe("B!1!6");
await dev.patch("c.ts", { find: "import.meta.hot.accept();", replace: "" });
await c.expectMessage("C"); // C self accepted even if the new one doesnt
expect(await c.js<string>`callFunction()`).toBe("B!2!5");
expect(await c.js<string>`callFunction()`).toBe("B!3!6");
},
});
devTest("import.meta.hot.accept specifier", {
timeoutMultiplier: 3,
files: {
"index.html": emptyHtmlFile({
scripts: ["a.ts"],
}),
// a
// b c
// d
"a.ts": `
import './b';
import './c';
console.log("A");
`,
"b.ts": `
import './d';
console.log("B");
import.meta.hot.accept("oh no", (newModule) => {
console.log('B:' + newModule.default);
})
`,
"c.ts": `
import './d';
console.log("C");
`,
"d.ts": `
console.log("D");
export default "hey!";
queueMicrotask(() => {
console.log("end");
});
`,
"unrelated.ts": `
export default "unrelated";
`,
},
async test(dev) {
{
await using c = await dev.client("/", {
errors: [
"b.ts:3:24: error: Dependencies to `import.meta.hot.accept` must be statically analyzable module specifiers matching direct imports.",
],
});
await dev.patch("b.ts", {
find: "oh no",
replace: "./d.ts",
errors: [
"b.ts:3:24: error: Dependencies to `import.meta.hot.accept` must be statically analyzable module specifiers matching direct imports.",
],
});
await c.expectReload(async () => {
await dev.patch("b.ts", { find: "./d.ts", replace: "./d" });
});
// Module evaluation order is guaranteed since there are no top-level
// await. `hmr-module.ts` does not use promises for synchronous ESM.
await c.expectMessage("D", "B", "C", "A", "end");
await c.expectReload(async () => {
// D -> C -> A causes a page reload.
await dev.write(
"d.ts",
`
console.log("D2");
export default "hey2!";
`,
);
});
await c.expectMessage("D2", "B", "C", "A");
}
await dev.write(
"c.ts",
`
import './d';
import './unrelated';
console.log("C");
import.meta.hot.accept();
`,
);
{
await using c = await dev.client("/");
await c.expectMessage("D2", "B", "C", "A");
await dev.write(
"d.ts",
`
console.log("D3");
export default "hey3!";
`,
);
await c.expectMessage("D3", "C", "B:hey3!");
await dev.write(
"c.ts",
`
import './d';
import './unrelated';
console.log("C");
import.meta.hot.accept("oh no", (newModule) => {
console.log('C:' + newModule.default);
});
`,
{
errors: [
"c.ts:4:24: error: Dependencies to `import.meta.hot.accept` must be statically analyzable module specifiers matching direct imports.",
],
},
);
await dev.patch("c.ts", {
find: "oh no",
replace: "./d",
});
await c.expectMessage("C"); // no-reload because prev self-accepted
await dev.write(
"d.ts",
`
console.log("D4");
export default "hey4!";
import.meta.hot.accept();
`,
);
// This order is guaranteed regardless of top-level await if it had existed.
await c.expectMessage("D4", "B:hey4!", "C:hey4!");
await dev.write(
"d.ts",
`
console.log("D5");
export default "hey5!";
import.meta.hot.accept();
`,
);
await c.expectMessage("D5", "B:hey5!", "C:hey5!");
await c.hardReload();
await c.expectMessage("D5", "B", "C", "A");
await dev.write(
"d.ts",
`
console.log("D6");
export default "hey6!";
import.meta.hot.accept();
`,
);
await c.expectMessage("D6", "B:hey6!", "C:hey6!");
}
},
});
devTest("import.meta.hot.accept multiple modules", {
files: {
"index.html": emptyHtmlFile({
scripts: ["index.ts"],
}),
"index.ts": `
import { count } from "./counter.ts";
import { name } from "./name.ts";
console.log("Initial: " + name + " " + count);
import.meta.hot.accept(["./counter.ts", "./name.ts"], (newModules) => {
if (newModules[0]) console.log("Counter updated: " + newModules[0].count);
if (newModules[1]) console.log("Name updated: " + newModules[1].name);
});
`,
"counter.ts": `
export const count = 1;
`,
"name.ts": `
export const name = "Alice";
`,
},
async test(dev) {
await using c = await dev.client("/");
await c.expectMessage("Initial: Alice 1");
await dev.write(
"counter.ts",
`
export const count = 2;
`,
);
await c.expectMessage("Counter updated: 2");
await dev.write(
"name.ts",
`
export const name = "Bob";
`,
);
await c.expectMessage("Name updated: Bob");
// Test updating both files
{
await using batch = await dev.batchChanges();
await dev.write(
"counter.ts",
`
export const count = 3;
`,
);
await dev.write(
"name.ts",
`
export const name = "Charlie";
`,
);
}
await c.expectMessageInAnyOrder("Counter updated: 3", "Name updated: Charlie");
},
});
devTest("import.meta.hot.data persistence", {
files: {
"index.html": emptyHtmlFile({
scripts: ["index.ts"],
}),
"index.ts": `
// Initialize or retrieve stored value
import.meta.hot.data.count ??= 0;
console.log("Initial count: " + import.meta.hot.data.count);
// Increment the count on each evaluation
import.meta.hot.data.count++;
// By using hot.data, you opt into implicit self-acceptance
`,
},
async test(dev) {
await using c = await dev.client("/");
await c.expectMessage("Initial count: 0");
await dev.writeNoChanges("index.ts");
await c.expectMessage("Initial count: 1");
await dev.writeNoChanges("index.ts");
await c.expectMessage("Initial count: 2");
await dev.writeNoChanges("index.ts");
await c.expectMessage("Initial count: 3");
},
});
devTest("import.meta.hot.dispose cleanup", {
files: {
"index.html": emptyHtmlFile({
scripts: ["index.ts"],
}),
"index.ts": `
console.log("Setting up");
const id = setInterval(() => {}, 1000);
import.meta.hot.dispose(() => {
console.log("Cleaning up");
clearInterval(id);
});
import.meta.hot.accept();
`,
},
async test(dev) {
await using c = await dev.client("/");
await c.expectMessage("Setting up");
await dev.write(
"index.ts",
`
console.log("Setting up again");
const id = setInterval(() => {}, 1000);
import.meta.hot.dispose(() => {
console.log("Cleaning up");
clearInterval(id);
});
import.meta.hot.accept();
`,
);
await c.expectMessage("Cleaning up", "Setting up again");
await dev.write(
"index.ts",
`
console.log("Third setup");
`,
);
await c.expectMessage("Cleaning up", "Third setup");
},
});
devTest("import.meta.hot invalid usage", {
files: {
"index.html": emptyHtmlFile({
scripts: ["index.ts"],
}),
"index.ts": `
const hot = import.meta.hot;
try {
hot.accept;
throw 'did not throw';
} catch (e) {
console.log(e?.message ?? e);
}
const accept = import.meta.hot.accept;
try {
accept("./something.ts", () => {});
throw 'did not throw';
} catch (e) {
console.log(e?.message ?? e);
}
const meta = import.meta;
try {
meta.hot.accept();
throw 'did not throw';
} catch (e) {
console.log(e?.message ?? e);
}
`,
},
async test(dev) {
await using c = await dev.client("/");
await c.expectMessage(
"import.meta.hot.accept cannot be used indirectly.",
'"import.meta.hot.accept" must be directly called with string literals for the specifiers. This way, the bundler can pre-process the arguments.',
"import.meta.hot cannot be used indirectly.",
);
},
});
devTest("import.meta.hot on/off events", {
files: {
"index.html": emptyHtmlFile({
scripts: ["index.ts"],
}),
"index.ts": `
console.log("Initial setup");
// Add event listener
import.meta.hot.on("vite:beforeUpdate", () => {
console.log("Before update event");
});
import.meta.hot.accept();
`,
},
async test(dev) {
await using c = await dev.client("/");
await c.expectMessage("Initial setup");
await dev.write(
"index.ts",
`
console.log("Updated setup");
// Events implementation is partial according to docs
import.meta.hot.on("vite:beforeUpdate", () => {
console.log("Before update event 2");
});
const handler = () => {
console.log("Another handler");
};
import.meta.hot.on("vite:beforeUpdate", handler);
// Remove the handler
import.meta.hot.off("vite:beforeUpdate", handler);
import.meta.hot.accept();
`,
);
await c.expectMessage("Updated setup");
await dev.write(
"index.ts",
`
console.log("Third update");
import.meta.hot.accept();
`,
);
await c.expectMessage("Third update");
},
});