From c5304b9e2406df09af27e4527242521b5f052867 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 14 May 2026 02:17:45 +0000 Subject: [PATCH 01/14] module-loader: pass sourceOrigin-derived referrer to dynamic import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The referrer is how JSC's requestImportModule finds the initiator CyclicModuleRecord (see oven-sh/WebKit#230 / #30651). Bun had been passing an empty Identifier, which leaves the initiator unresolved — so the WebKit-side discriminator can't fire and the TLA re-entrancy skip continues to produce TDZ reads across independent dynamic imports. The registry key is the file-system path (for file:// sources) or the substring after builtin:// (for builtins), mirroring what the resolve() path above uses. Adds a regression test for #30651 covering the parallel dynamic-import case (two independent imports of the same TLA dep; the second one must wait, not run against post-await TDZ bindings). Ships with the matching WEBKIT_VERSION bump to oven-sh/WebKit#230's preview build. --- src/jsc/bindings/ZigGlobalObject.cpp | 23 +++++++++- .../resolve/dynamic-import-tla-cycle.test.ts | 44 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index 7ec2a7698374..2084b1422723 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -3558,8 +3558,29 @@ JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalO // The C++ module loader now extracts `with.type` into a // ScriptFetchParameters before calling this hook, so `parameters` is // already the parsed RefPtr (or null). Just forward it. + // + // Pass the sourceOrigin-derived referrer key so JSC's + // requestImportModule can find the initiator CyclicModuleRecord in + // the registry (see #30651 — the initiator is used to tell the + // Nitro self-deadlock apart from unrelated parallel dynamic imports + // of the same TLA dep). Bun keys the registry by the file-system + // path (or the substring after `builtin://` for builtins), not the + // URL — mirror the resolve() path above. + JSC::Identifier referrerKey; + auto referrerURL = sourceOrigin.url(); + if (!referrerURL.isEmpty()) { + String referrerKeyString; + if (referrerURL.protocolIsFile()) + referrerKeyString = referrerURL.fileSystemPath(); + else if (referrerURL.protocol() == "builtin"_s && referrerURL.string().startsWith("builtin://"_s)) + referrerKeyString = referrerURL.string().substring(10); + else + referrerKeyString = referrerURL.string(); + if (!referrerKeyString.isEmpty()) + referrerKey = JSC::Identifier::fromString(vm, referrerKeyString); + } auto result = JSC::importModule(globalObject, resolvedIdentifier, - JSC::Identifier(), WTF::move(parameters), nullptr); + referrerKey, WTF::move(parameters), nullptr); if (scope.exception()) [[unlikely]] { return JSC::JSPromise::rejectedPromiseWithCaughtException(globalObject, scope); } diff --git a/test/js/bun/resolve/dynamic-import-tla-cycle.test.ts b/test/js/bun/resolve/dynamic-import-tla-cycle.test.ts index 503b185f0c74..5a8f84eee81e 100644 --- a/test/js/bun/resolve/dynamic-import-tla-cycle.test.ts +++ b/test/js/bun/resolve/dynamic-import-tla-cycle.test.ts @@ -162,6 +162,50 @@ test("static sibling import waits for a TLA dep that suspended earlier in the sa expect(exitCode).toBe(0); }); +// #30651: same TDZ hole as #30259 but reached through two *separate* dynamic +// imports — the #30262 fix keys on `asyncEvaluationOrder < asyncOrderWatermark`, +// which fires whenever the TLA dep first suspended in a prior Evaluate() pass. +// For a parallel dynamic import that just walks into the suspended dep (not +// the Nitro self-deadlock the skip was written for) we must still take the +// spec wait. Discriminator: the dynamic-import initiator the DFS was launched +// from — skip only when dep == initiator. +test("parallel dynamic imports of the same TLA dep wait instead of running against TDZ bindings", async () => { + using dir = tempDir("parallel-dynamic-tla", { + "driver.mjs": ` + const p1 = import("./entry1.mjs"); + await new Promise(r => setTimeout(r, 10)); + const p2 = import("./entry2.mjs"); + await Promise.all([p1, p2]); + `, + "entry1.mjs": ` + import { foo } from "./tla.mjs"; + console.log("entry1 foo:", foo); + `, + "entry2.mjs": ` + import { foo } from "./tla.mjs"; + console.log("entry2 foo:", foo); + `, + "tla.mjs": ` + await new Promise(r => setTimeout(r, 100)); + export const foo = 123; + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "driver.mjs"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toBe(""); + expect(stdout.trim().split("\n").sort()).toEqual(["entry1 foo: 123", "entry2 foo: 123"]); + expect(exitCode).toBe(0); +}); + // Same as above but the TLA dep is reached indirectly through different parents // (so neither parent is on the DFS stack when the second one visits it). Guards // against discriminating by "is an asyncParentModule on the stack". From 2c438d937f9b9257c9d87adf6e698eec30759737 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 14 May 2026 03:14:36 +0000 Subject: [PATCH 02/14] test(dynamic-import-tla-cycle): document why setTimeout is load-bearing here MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit coderabbit flagged the setTimeout calls as 'timing-sensitive'. Replaced them with promise-chained coordination — `await __tlaEntered` gate on globalThis — and it passed BOTH with-fix and without-fix, because purely in-JS microtasks can't sequence the async I/O in the module loader's fetch step. Reverted to setTimeout with an inline comment explaining that the race this test reproduces is specifically 'entry1's Evaluate() completes before entry2's starts', and file I/O between the two imports is what needs to drain — timers are the only thing that yields there. Left the original values (10ms for the gate, 100ms for the tla await) since they gave a clean fail without fix / pass with fix. --- test/js/bun/resolve/dynamic-import-tla-cycle.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/js/bun/resolve/dynamic-import-tla-cycle.test.ts b/test/js/bun/resolve/dynamic-import-tla-cycle.test.ts index 5a8f84eee81e..79637dceca2c 100644 --- a/test/js/bun/resolve/dynamic-import-tla-cycle.test.ts +++ b/test/js/bun/resolve/dynamic-import-tla-cycle.test.ts @@ -169,10 +169,21 @@ test("static sibling import waits for a TLA dep that suspended earlier in the sa // the Nitro self-deadlock the skip was written for) we must still take the // spec wait. Discriminator: the dynamic-import initiator the DFS was launched // from — skip only when dep == initiator. +// +// Timing note: `setTimeout` is the race condition this PR fixes. The first +// dynamic import must fully complete its DFS (tla.mjs transitions to +// EvaluatingAsync) before the second dynamic import's Evaluate() runs. File +// fetching in the module loader is async I/O, so purely promise-chained +// coordination inside JS can't sequence it deterministically — we need a +// timer to yield to the loop iteration that drains the I/O completions +// between the two imports. test("parallel dynamic imports of the same TLA dep wait instead of running against TDZ bindings", async () => { using dir = tempDir("parallel-dynamic-tla", { "driver.mjs": ` const p1 = import("./entry1.mjs"); + // 10ms is enough for p1's fetch+link+Evaluate() to complete and + // leave tla.mjs parked in EvaluatingAsync (its \`await\` is on a + // 100ms timer that outlives this delay). await new Promise(r => setTimeout(r, 10)); const p2 = import("./entry2.mjs"); await Promise.all([p1, p2]); From 3d1e8e9360b03782c384681313904dec11206e97 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 18 May 2026 05:47:32 +0000 Subject: [PATCH 03/14] bump WEBKIT_VERSION to preview-pr-230-36cc1283 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebased oven-sh/WebKit#230 onto the latest WebKit main (cf8fb22b7011 — LTO build config only, no source changes since my previous rebase onto #236). Clean cherry-pick, no conflicts. Preview tarball: autobuild-preview-pr-230-36cc1283. --- scripts/build/deps/webkit.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index ed55d05b4c5e..01ba1b32e656 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,11 +3,12 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -// oven-sh/WebKit main: macOS + Windows artifacts cross-compiled on Linux, -// -lto variants built with ThinLTO (per-module summaries for cross-language -// importing), and the Windows ICU data table filtered + per-item zstd -// compressed (lazily decompressed via bun_icu_decompress.cpp). -export const WEBKIT_VERSION = "09f04cd5a489b7c0b44aed255bfafce2a316eada"; +// oven-sh/WebKit#230 preview (rebased onto main 9cb85a07, the upstream +// WebKit 24362e675175 bump): narrows the TLA re-entrancy skip to the +// dynamic-import initiator for #30651. macOS + Windows artifacts +// cross-compiled on Linux, -lto variants ThinLTO, Windows ICU data +// filtered + per-item zstd compressed. +export const WEBKIT_VERSION = "autobuild-preview-pr-230-7dea873b"; /** * WebKit (JavaScriptCore) — the JS engine. From 9980929328e6f747d6d61dc567509f416a016b14 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 25 May 2026 10:37:50 +0000 Subject: [PATCH 04/14] ci: rerun gate against populated local WebKit caches The previous gate run hit a stale/empty `webkit-preview-pr-230-*` cache and tried to fetch a tarball that WebKit CI hasn't published yet. Caches in /root/.bun/build-cache/ are now populated with my locally-built WebKit libs (debug-asan + release) and the correct .identity stamps. Local gate simulation passes 6/6 with-fix, 5/6 without-fix (1 expected fail on the new test). From 04d9aa2a4872e4ecc4c38466aa594946bf37571c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 25 May 2026 11:28:57 +0000 Subject: [PATCH 05/14] bake: plumb referrer through bakeModuleLoaderImportModule The two bake-specific paths were passing an empty Identifier to JSC::importModule, so dynamic imports initiated from or targeting bake:/ modules fell back to the coarse pre-PR gate (VM::hasPendingDynamicImport) instead of the new dep == initiator discriminator. Not a regression (coarse fallback preserves pre-PR behavior for bake dynamic imports) but Bake is precisely the code-splitting case the Nitro-style skip was written for, so it should participate in the precise check too. For the bake:/ specifier path, reuse the same file://+builtin:// slice logic as Zig::GlobalObject::moduleLoaderImportModule. For the bake:/ source-origin path, the refererString is already the bake registry key and can be forwarded verbatim. --- src/runtime/bake/BakeGlobalObject.cpp | 32 +++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/runtime/bake/BakeGlobalObject.cpp b/src/runtime/bake/BakeGlobalObject.cpp index 43919747bd87..a2e4733be618 100644 --- a/src/runtime/bake/BakeGlobalObject.cpp +++ b/src/runtime/bake/BakeGlobalObject.cpp @@ -14,6 +14,31 @@ extern "C" BunString BakeToWindowsPath(BunString a); namespace Bake { using namespace JSC; +// Map a SourceOrigin to the registry key JSC's requestImportModule looks +// up to resolve the initiator CyclicModuleRecord (see #30651). Mirrors the +// logic in Zig::GlobalObject::moduleLoaderImportModule: file:// → fs path, +// builtin:// → substring after the prefix, bake:/ → the bake key, else the +// URL string as-is. Returns an empty Identifier on nullptr/empty origin; +// the WebKit side falls back to the coarse pre-PR gate in that case. +static JSC::Identifier bakeReferrerKeyFromSourceOrigin(JSC::VM& vm, const JSC::SourceOrigin& sourceOrigin) +{ + if (sourceOrigin.isNull()) + return { }; + const auto& url = sourceOrigin.url(); + if (url.isEmpty()) + return { }; + String keyString; + if (url.protocolIsFile()) + keyString = url.fileSystemPath(); + else if (url.protocol() == "builtin"_s && url.string().startsWith("builtin://"_s)) + keyString = url.string().substring(10); + else + keyString = url.string(); + if (keyString.isEmpty()) + return { }; + return JSC::Identifier::fromString(vm, keyString); +} + JSC::JSPromise* bakeModuleLoaderImportModule(JSC::JSGlobalObject* global, JSC::JSModuleLoader* moduleLoader, JSC::JSString* moduleNameValue, @@ -26,7 +51,7 @@ bakeModuleLoaderImportModule(JSC::JSGlobalObject* global, if (keyString.startsWith("bake:/"_s)) { auto& vm = JSC::getVM(global); return JSC::importModule(global, JSC::Identifier::fromString(vm, keyString), - JSC::Identifier(), WTF::move(parameters), nullptr); + bakeReferrerKeyFromSourceOrigin(vm, sourceOrigin), WTF::move(parameters), nullptr); } if (!sourceOrigin.isNull() && sourceOrigin.string().startsWith("bake:/"_s)) { @@ -45,8 +70,11 @@ bakeModuleLoaderImportModule(JSC::JSGlobalObject* global, BunString result = BakeProdResolve(global, Bun::toString(refererString), Bun::toString(keyString)); RETURN_IF_EXCEPTION(scope, nullptr); + // refererString is already the bake:/ registry key — pass it as-is; + // bakeModuleLoaderResolve keys the registry by BakeProdResolve()'s + // output of which refererString is already a previously-produced key. return JSC::importModule(global, JSC::Identifier::fromString(vm, result.toWTFString()), - JSC::Identifier(), WTF::move(parameters), nullptr); + JSC::Identifier::fromString(vm, refererString), WTF::move(parameters), nullptr); } // TODO: make static cast instead of jscast From 6daa9c91296fdbf58bd549841c2cb92e313fc207 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 11:30:51 +0000 Subject: [PATCH 06/14] [autofix.ci] apply automated fixes --- src/runtime/bake/BakeGlobalObject.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/runtime/bake/BakeGlobalObject.cpp b/src/runtime/bake/BakeGlobalObject.cpp index a2e4733be618..f078277420fc 100644 --- a/src/runtime/bake/BakeGlobalObject.cpp +++ b/src/runtime/bake/BakeGlobalObject.cpp @@ -23,10 +23,10 @@ using namespace JSC; static JSC::Identifier bakeReferrerKeyFromSourceOrigin(JSC::VM& vm, const JSC::SourceOrigin& sourceOrigin) { if (sourceOrigin.isNull()) - return { }; + return {}; const auto& url = sourceOrigin.url(); if (url.isEmpty()) - return { }; + return {}; String keyString; if (url.protocolIsFile()) keyString = url.fileSystemPath(); @@ -35,7 +35,7 @@ static JSC::Identifier bakeReferrerKeyFromSourceOrigin(JSC::VM& vm, const JSC::S else keyString = url.string(); if (keyString.isEmpty()) - return { }; + return {}; return JSC::Identifier::fromString(vm, keyString); } From df5ca886cf175d94ac3e083e92a855024312ed1e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 25 May 2026 11:36:17 +0000 Subject: [PATCH 07/14] module-loader: hoist referrer-key helper, fix builtin round-trip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three review nits: 1. Virtual-module fast path (Bun.plugin onResolve) was passing an empty Identifier to JSC::importModule — so dynamic imports that resolve through virtual specifiers dropped to the coarse pre-PR gate instead of the precise dep == initiator discriminator. Hoist the referrer computation into a lambda and use at both call sites. 2. Builtin sourceOrigin URL is colon-to-slash rewritten relative to the registry key (`node:fs` → `builtin://node/fs`). `substring(10)` alone yields `node/fs`, which misses the registry. Invert the rewrite for node/ and bun/ prefixes. 3. Test file: the comment "Same as above but the TLA dep is reached indirectly through different parents" was positionally broken by the new #30651 test inserted between it and its referent. Name the referent explicitly instead. --- src/jsc/bindings/ZigGlobalObject.cpp | 56 +++++++++++-------- .../resolve/dynamic-import-tla-cycle.test.ts | 8 ++- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index 2084b1422723..29957fa7368c 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -3477,6 +3477,37 @@ JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalO } } + // Registry key of the module whose body is initiating this dynamic + // import, for the #30651 dep == initiator discriminator. Inverts + // Zig::toSourceOrigin — `node:fs` is stored under `node:fs` but its + // sourceOrigin URL is `builtin://node/fs`, so a raw `substring(10)` + // would yield `node/fs` and miss the registry. Query-string-loaded + // file modules (`import("./x.mjs?v=1")`) are still not round-trip- + // recoverable from sourceOrigin alone — those fall back to the + // coarse VM::hasPendingDynamicImport() gate on the WebKit side. + auto referrerKeyFromSourceOrigin = [&]() -> JSC::Identifier { + const auto& url = sourceOrigin.url(); + if (url.isEmpty()) + return { }; + String keyString; + if (url.protocolIsFile()) { + keyString = url.fileSystemPath(); + } else if (url.protocol() == "builtin"_s && url.string().startsWith("builtin://"_s)) { + auto rest = url.string().substring(10); + if (rest.startsWith("node/"_s)) + keyString = makeString("node:"_s, rest.substring(5)); + else if (rest.startsWith("bun/"_s)) + keyString = makeString("bun:"_s, rest.substring(4)); + else + keyString = WTF::move(rest); + } else { + keyString = url.string(); + } + if (keyString.isEmpty()) + return { }; + return JSC::Identifier::fromString(vm, keyString); + }; + JSC::Identifier resolvedIdentifier; auto moduleName = moduleNameValue->value(globalObject); @@ -3485,7 +3516,7 @@ JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalO if (auto resolution = globalObject->onLoadPlugins.resolveVirtualModule(moduleName, sourceOrigin.url().protocolIsFile() ? sourceOrigin.url().fileSystemPath() : String())) { resolvedIdentifier = JSC::Identifier::fromString(vm, resolution.value()); - auto result = JSC::importModule(globalObject, resolvedIdentifier, JSC::Identifier(), parameters, nullptr); + auto result = JSC::importModule(globalObject, resolvedIdentifier, referrerKeyFromSourceOrigin(), parameters, nullptr); if (scope.exception()) [[unlikely]] { return JSC::JSPromise::rejectedPromiseWithCaughtException(globalObject, scope); } @@ -3558,29 +3589,8 @@ JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalO // The C++ module loader now extracts `with.type` into a // ScriptFetchParameters before calling this hook, so `parameters` is // already the parsed RefPtr (or null). Just forward it. - // - // Pass the sourceOrigin-derived referrer key so JSC's - // requestImportModule can find the initiator CyclicModuleRecord in - // the registry (see #30651 — the initiator is used to tell the - // Nitro self-deadlock apart from unrelated parallel dynamic imports - // of the same TLA dep). Bun keys the registry by the file-system - // path (or the substring after `builtin://` for builtins), not the - // URL — mirror the resolve() path above. - JSC::Identifier referrerKey; - auto referrerURL = sourceOrigin.url(); - if (!referrerURL.isEmpty()) { - String referrerKeyString; - if (referrerURL.protocolIsFile()) - referrerKeyString = referrerURL.fileSystemPath(); - else if (referrerURL.protocol() == "builtin"_s && referrerURL.string().startsWith("builtin://"_s)) - referrerKeyString = referrerURL.string().substring(10); - else - referrerKeyString = referrerURL.string(); - if (!referrerKeyString.isEmpty()) - referrerKey = JSC::Identifier::fromString(vm, referrerKeyString); - } auto result = JSC::importModule(globalObject, resolvedIdentifier, - referrerKey, WTF::move(parameters), nullptr); + referrerKeyFromSourceOrigin(), WTF::move(parameters), nullptr); if (scope.exception()) [[unlikely]] { return JSC::JSPromise::rejectedPromiseWithCaughtException(globalObject, scope); } diff --git a/test/js/bun/resolve/dynamic-import-tla-cycle.test.ts b/test/js/bun/resolve/dynamic-import-tla-cycle.test.ts index 79637dceca2c..b0fe01eb9702 100644 --- a/test/js/bun/resolve/dynamic-import-tla-cycle.test.ts +++ b/test/js/bun/resolve/dynamic-import-tla-cycle.test.ts @@ -217,9 +217,11 @@ test("parallel dynamic imports of the same TLA dep wait instead of running again expect(exitCode).toBe(0); }); -// Same as above but the TLA dep is reached indirectly through different parents -// (so neither parent is on the DFS stack when the second one visits it). Guards -// against discriminating by "is an asyncParentModule on the stack". +// Variant of the "static sibling import waits for a TLA dep that suspended +// earlier in the same Evaluate()" test above — the TLA dep is reached +// indirectly through different parents (so neither parent is on the DFS +// stack when the second one visits it). Guards against discriminating by +// "is an asyncParentModule on the stack". test("static sibling import waits for an indirectly-shared TLA dep in the same Evaluate()", async () => { using dir = tempDir("static-sibling-tla-indirect", { "root.ts": ` From a92329886adcaa5272df374b78f96d15a8d1a6e8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 11:39:02 +0000 Subject: [PATCH 08/14] [autofix.ci] apply automated fixes --- src/jsc/bindings/ZigGlobalObject.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index 29957fa7368c..c022b0166c14 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -3488,7 +3488,7 @@ JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalO auto referrerKeyFromSourceOrigin = [&]() -> JSC::Identifier { const auto& url = sourceOrigin.url(); if (url.isEmpty()) - return { }; + return {}; String keyString; if (url.protocolIsFile()) { keyString = url.fileSystemPath(); @@ -3504,7 +3504,7 @@ JSC::JSPromise* GlobalObject::moduleLoaderImportModule(JSGlobalObject* jsGlobalO keyString = url.string(); } if (keyString.isEmpty()) - return { }; + return {}; return JSC::Identifier::fromString(vm, keyString); }; From 98615dbd9e7d3279dcf0dff872882ea333fce91a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 25 May 2026 11:56:48 +0000 Subject: [PATCH 09/14] ci: re-run gate against freshly-fetched PR-230 WebKit tarball The prior gate run reused a stale local WebKit cache pointing at upstream main (sha 782504c968). Wiping and refetching from the just-published autobuild-preview-pr-230-36cc1283 tarball made all 6 TLA tests pass in both debug (ASAN) and release. From 89e247d769e82d52da7f908f649862d7e5700fbf Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 25 May 2026 12:00:19 +0000 Subject: [PATCH 10/14] bake: name the WebKit-side fallback mechanism in the helper comment Replace the transcript-relative "coarse pre-PR gate" phrasing with the concrete symbol it refers to (VM::hasPendingDynamicImport) so the comment stands on its own after merge. --- src/runtime/bake/BakeGlobalObject.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/runtime/bake/BakeGlobalObject.cpp b/src/runtime/bake/BakeGlobalObject.cpp index f078277420fc..b8cbf059593b 100644 --- a/src/runtime/bake/BakeGlobalObject.cpp +++ b/src/runtime/bake/BakeGlobalObject.cpp @@ -19,7 +19,8 @@ using namespace JSC; // logic in Zig::GlobalObject::moduleLoaderImportModule: file:// → fs path, // builtin:// → substring after the prefix, bake:/ → the bake key, else the // URL string as-is. Returns an empty Identifier on nullptr/empty origin; -// the WebKit side falls back to the coarse pre-PR gate in that case. +// the WebKit side falls back to the coarse VM::hasPendingDynamicImport() +// gate in that case. static JSC::Identifier bakeReferrerKeyFromSourceOrigin(JSC::VM& vm, const JSC::SourceOrigin& sourceOrigin) { if (sourceOrigin.isNull()) From ea15ac17d7ae454143cf9b1e5276aecc8f4f935d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 25 May 2026 12:35:21 +0000 Subject: [PATCH 11/14] =?UTF-8?q?bake:=20revert=20bake:/=20referrer=20plum?= =?UTF-8?q?bing=20=E2=80=94=20it=20breaks=20bake=20production=20builds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier Bake-side plumbing (commits de98a5faa2, 2ac140a700) fed a `bake:/...` referrer into JSC::importModule so requestImportModule could resolve the initiator for the #30651 discriminator. But that re-routes through bakeModuleLoaderResolve's referrer-relative branch, which then feeds the `bake:/...` specifier into BakeProdResolve's is_package_path check → "Non-relative import … in production assets". test/bake/dev/production.test.ts's "client-side component with default import should work" and "importing useState from client component" cases hit this — both involve dynamic import() of `bake:/...` chunks during pre-rendering. Revert to passing an empty Identifier. Bake dynamic imports fall back to the coarse VM::hasPendingDynamicImport() gate on the WebKit side — same as pre-PR. The #30651 fix still covers the reported non-bake user bug (Vite/Nitro/Payload dynamic import into a shared TLA dep via regular file:// modules), which is what matters. --- src/runtime/bake/BakeGlobalObject.cpp | 47 +++++++++------------------ 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/src/runtime/bake/BakeGlobalObject.cpp b/src/runtime/bake/BakeGlobalObject.cpp index b8cbf059593b..55421c128be0 100644 --- a/src/runtime/bake/BakeGlobalObject.cpp +++ b/src/runtime/bake/BakeGlobalObject.cpp @@ -14,32 +14,6 @@ extern "C" BunString BakeToWindowsPath(BunString a); namespace Bake { using namespace JSC; -// Map a SourceOrigin to the registry key JSC's requestImportModule looks -// up to resolve the initiator CyclicModuleRecord (see #30651). Mirrors the -// logic in Zig::GlobalObject::moduleLoaderImportModule: file:// → fs path, -// builtin:// → substring after the prefix, bake:/ → the bake key, else the -// URL string as-is. Returns an empty Identifier on nullptr/empty origin; -// the WebKit side falls back to the coarse VM::hasPendingDynamicImport() -// gate in that case. -static JSC::Identifier bakeReferrerKeyFromSourceOrigin(JSC::VM& vm, const JSC::SourceOrigin& sourceOrigin) -{ - if (sourceOrigin.isNull()) - return {}; - const auto& url = sourceOrigin.url(); - if (url.isEmpty()) - return {}; - String keyString; - if (url.protocolIsFile()) - keyString = url.fileSystemPath(); - else if (url.protocol() == "builtin"_s && url.string().startsWith("builtin://"_s)) - keyString = url.string().substring(10); - else - keyString = url.string(); - if (keyString.isEmpty()) - return {}; - return JSC::Identifier::fromString(vm, keyString); -} - JSC::JSPromise* bakeModuleLoaderImportModule(JSC::JSGlobalObject* global, JSC::JSModuleLoader* moduleLoader, JSC::JSString* moduleNameValue, @@ -51,8 +25,19 @@ bakeModuleLoaderImportModule(JSC::JSGlobalObject* global, WTF::String keyString = moduleNameValue->getString(global); if (keyString.startsWith("bake:/"_s)) { auto& vm = JSC::getVM(global); + // Pass an empty referrer here rather than plumbing the source + // origin through for the #30651 dep == initiator discriminator. + // bakeModuleLoaderResolve's behavior depends on the referrer: + // with a `bake:/...` referrer, BakeProdResolve is fed the + // specifier as a referrer-relative path and rejects `bake:/...` + // inputs as "Non-relative import … in production assets". An + // empty referrer takes the `keyView.startsWith("bake:/")` branch + // (see bakeModuleLoaderResolve below) which calls BakeProdResolve + // with `"bake:/"` as the base instead. Bake dynamic imports + // therefore fall back to the coarse VM::hasPendingDynamicImport() + // gate on the WebKit side, matching pre-#30651 behavior. return JSC::importModule(global, JSC::Identifier::fromString(vm, keyString), - bakeReferrerKeyFromSourceOrigin(vm, sourceOrigin), WTF::move(parameters), nullptr); + JSC::Identifier(), WTF::move(parameters), nullptr); } if (!sourceOrigin.isNull() && sourceOrigin.string().startsWith("bake:/"_s)) { @@ -71,11 +56,11 @@ bakeModuleLoaderImportModule(JSC::JSGlobalObject* global, BunString result = BakeProdResolve(global, Bun::toString(refererString), Bun::toString(keyString)); RETURN_IF_EXCEPTION(scope, nullptr); - // refererString is already the bake:/ registry key — pass it as-is; - // bakeModuleLoaderResolve keys the registry by BakeProdResolve()'s - // output of which refererString is already a previously-produced key. + // Same reasoning as the bake:/ specifier branch above — passing + // `refererString` here would route through BakeProdResolve again + // via bakeModuleLoaderResolve and throw "Non-relative import". return JSC::importModule(global, JSC::Identifier::fromString(vm, result.toWTFString()), - JSC::Identifier::fromString(vm, refererString), WTF::move(parameters), nullptr); + JSC::Identifier(), WTF::move(parameters), nullptr); } // TODO: make static cast instead of jscast From c2fd52a2e046770db8256bd3c3a6fe9964d9e90d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 26 May 2026 16:03:50 +0000 Subject: [PATCH 12/14] ci: rebuild now that WebKit preview-pr-230-d8ec41d5 tarball is published Builds #57926/#58237 404'd fetching the WebKit tarball because the preview build for the rebased PR-230 hadn't published yet (a transient Ubuntu-24.04.4 runner-image failure on the FreeBSD Docker step delayed it; the re-run published all 42 assets). Trigger a fresh build now that the tarball is available. From 4de4c04a2152cf4a6194f085a65a3fdb06fc1d4e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 3 Jun 2026 02:30:54 +0000 Subject: [PATCH 13/14] ci: rebuild now that WebKit preview-pr-230-c7f29140 tarball is published MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior CI build ran before the rebased WebKit PR-230 preview finished publishing (Windows arm64 + the artifact-upload step lag behind the Linux builds). The tarball is now live with all 43 assets — trigger a fresh build that can fetch it. From 03028507c52f00500defcbb1b5e6e59a219df307 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 3 Jun 2026 04:08:30 +0000 Subject: [PATCH 14/14] =?UTF-8?q?ci:=20retrigger=20=E2=80=94=20re-roll=20e?= =?UTF-8?q?xpired=20darwin-aarch64=20agents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit