mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
Co-authored-by: Zack Radisic <zack@theradisic.com> Co-authored-by: zackradisic <56137411+zackradisic@users.noreply.github.com>
473 lines
13 KiB
TypeScript
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");
|
|
},
|
|
});
|