From 4922727b7c9b64ae6c914f1acc0b0e5a67560620 Mon Sep 17 00:00:00 2001 From: robobun Date: Wed, 1 Jul 2026 04:55:11 +0000 Subject: [PATCH] Test: a failed dynamic import must not strand later importers of the same file Regression test for the hang fixed by oven-sh/WebKit#262, which is in the current WEBKIT_VERSION. // bad.ts contains "import {" (one parser error -> a BuildMessage) await import("./bad.ts").catch(() => {}); await import("./other.ts"); // other.ts: import "./bad.ts"; never settled A top-level dynamic import whose transpile fails with a single parser error rejects with a BuildMessage, which is not an ErrorInstance. JSC's moduleLoadTopSettled gated its fetch-error classification on dynamicDowncast, so nothing was recorded and moduleLoadTopRejected fell back to setEvaluationError. That left the module registry entry with no fetch, module, or load promise; the next importer of the same file that goes through hostLoadImportedModule (a static import, or a dynamic import inside another module's graph load) then parked forever on a freshly created fetchPromise nothing settles. --- .../resolve/concurrent-dynamic-import.test.ts | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/test/js/bun/resolve/concurrent-dynamic-import.test.ts b/test/js/bun/resolve/concurrent-dynamic-import.test.ts index b21ead8ddde6..4faff242dc06 100644 --- a/test/js/bun/resolve/concurrent-dynamic-import.test.ts +++ b/test/js/bun/resolve/concurrent-dynamic-import.test.ts @@ -6,7 +6,7 @@ import { bunEnv, bunExe, tempDir } from "harness"; // each call gets its own embedder fetch promise (the registry entry is created // only after the first fetch settles), so the loser of that race must still be // resolved by Bun__onFulfillAsyncModule rather than left pending forever. -test("concurrent dynamic imports of the same module both resolve", async () => { +test.concurrent("concurrent dynamic imports of the same module both resolve", async () => { using dir = tempDir("concurrent-dyn-import", { "shared.ts": `export const heavy = "H";`, "modules.ts": `import { heavy } from "./shared";\nexport const lazy = heavy + "-lazy";`, @@ -30,3 +30,41 @@ test("concurrent dynamic imports of the same module both resolve", async () => { expect(stdout.trim()).toBe("ok"); expect(exitCode).toBe(0); }); + +// A top-level dynamic import whose fetch rejects with a non-Error value (a +// single-message transpile failure is a BuildMessage, not an ErrorInstance) +// must record a fetch failure on its registry entry. If it is recorded as an +// evaluation error instead, the entry is left with no fetch/module/load +// promise, and every later importer of the same file that goes through +// hostLoadImportedModule parks forever on a promise nothing settles. +test.concurrent("a failed dynamic import does not strand later static importers of the same file", async () => { + using dir = tempDir("dyn-import-fetch-error", { + // Exactly one parser error so the rejection value is a BuildMessage + // rather than an AggregateError (which is an ErrorInstance). + "bad.ts": `import {\n`, + "other.ts": `import "./bad.ts";\nconsole.log("other loaded");\n`, + "entry.mjs": ` + const d = import.meta.dir; + const names = []; + try { await import(d + "/bad.ts"); names.push("resolved"); } catch (e) { names.push(e?.name); } + try { await import(d + "/other.ts"); names.push("resolved"); } catch (e) { names.push(e?.name); } + console.log(JSON.stringify(names)); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "entry.mjs"], + cwd: String(dir), + env: bunEnv, + stdio: ["ignore", "pipe", "pipe"], + }); + // Without the fix the second import() never settles, so the child never + // exits and this test times out. + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout: stdout.trim(), stderr, exitCode, signalCode: proc.signalCode }).toEqual({ + stdout: JSON.stringify(["BuildMessage", "BuildMessage"]), + stderr: "", + exitCode: 0, + signalCode: null, + }); +});