diff --git a/src/runtime/bake/hmr-module.ts b/src/runtime/bake/hmr-module.ts index 2b5b6e6a01b0..b9184bb5044a 100644 --- a/src/runtime/bake/hmr-module.ts +++ b/src/runtime/bake/hmr-module.ts @@ -355,6 +355,8 @@ export function loadModuleSync(id: Id, isUserDynamic: boolean, importer: HMRModu } const { [ESMProps.imports]: deps, [ESMProps.load]: load, [ESMProps.isAsync]: isAsync } = loadOrEsmModule; if (isAsync) { + // A stale module is refused before its body runs, so the next load evaluates it. + if (mod) mod.state = State.Stale; throw new AsyncImportError(id); } if (!mod) { diff --git a/test/bake/dev/esm.test.ts b/test/bake/dev/esm.test.ts index 240b9b6ff50d..eb6b396b9894 100644 --- a/test/bake/dev/esm.test.ts +++ b/test/bake/dev/esm.test.ts @@ -362,6 +362,71 @@ devTest("cannot require a module with top level await", { }); }, }); +devTest("import() evaluates a module with top-level await after require() refused it", { + framework: minimalFramework, + files: { + // CommonJS, so that the next load evaluates it again after it threw. + "flaky.ts": ` + if (!globalThis.flakyThrew) { + globalThis.flakyThrew = true; + throw new Error("flaky failed"); + } + module.exports = { flaky: "flaky" }; + `, + "tla.ts": ` + import { flaky } from "./flaky"; + await 0; + export const tla = "tla(" + flaky + ")"; + `, + // It has no await. A require() of it gets to "tla" through an import. + "via.ts": ` + import { tla } from "./tla"; + export const via = "via(" + tla + ")"; + `, + "routes/index.ts": ` + function requireIt(load) { + try { + load(); + return "required"; + } catch (e) { + return e.message; + } + } + async function importVia() { + try { + const ns = await import("../via"); + return ns === null ? "null" : ns.via; + } catch (e) { + return e.message; + } + } + export default async function () { + return Response.json([ + // No load of "tla" ran before these two. + requireIt(() => require("../tla")), + requireIt(() => require("../via")), + // "flaky" throws the first time. The body of "tla" does not run, and "tla" is stale. + await importVia(), + requireIt(() => require("../tla")), + requireIt(() => require("../via")), + await importVia(), + ]); + } + `, + }, + async test(dev) { + const refused = (id: string) => + `Cannot require "${id}" because "tla.ts" uses top-level await, but 'require' is a synchronous operation.`; + expect(await dev.fetch("/").json()).toEqual([ + refused("tla.ts"), + refused("via.ts"), + "flaky failed", + refused("tla.ts"), + refused("via.ts"), + "via(tla(flaky))", + ]); + }, +}); devTest("function that is assigned to should become a live binding", { files: { "index.html": emptyHtmlFile({