// 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`callFunction()`).toBe("A!0!0"); expect(await c.js`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`callFunction()`).toBe("A!0!5"); expect(await c.js`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`callFunction()`).toBe("B!0!7"); expect(await c.js`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`callFunction()`).toBe("B!0!5"); expect(await c.js`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`callFunction()`).toBe("B!2!5"); expect(await c.js`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"); }, });