Skip to content

moduleLoaderImportModule: thread referrer asyncEvaluationOrder for TLA self-deadlock skip - #32437

Merged
Jarred-Sumner merged 4 commits into
mainfrom
claude/tla-referrer-async-order
Jun 17, 2026
Merged

moduleLoaderImportModule: thread referrer asyncEvaluationOrder for TLA self-deadlock skip#32437
Jarred-Sumner merged 4 commits into
mainfrom
claude/tla-referrer-async-order

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Bun-side half of oven-sh/WebKit claude/tla-referrer-async-order (supersedes oven-sh/WebKit#228). Fixes #30634.

What

moduleLoaderImportModule looks up the referrer's asyncEvaluationOrder via the new JSModuleLoader::asyncEvaluationOrderForKey() and passes it to JSC::importModule as the new referrerAsyncOrder param. The resolve() referrer stays empty (JSC::Identifier()), so plugin onResolve and the second resolve pass are unchanged.

The sourceOrigin → path computation is hoisted so the virtual-module branch and the resolve block share it.

Tests

test/js/bun/resolve/dynamic-import-tla-cycle.test.ts — adds the #30634 sibling-dynamic-import test (fails on system Bun with ReferenceError: Cannot access 'wrapped' before initialization, passes on this build). The 5 existing tests (2 deadlock, 3 #30259 sibling) still pass.

Depends on the WebKit PR landing + bumping scripts/build/deps/webkit.ts. This commit compiles against current WebKit (the new param is defaulted) but the deadlock tests will hang until the WebKit side lands.

@robobun

robobun commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator
Updated 8:53 PM PT - Jun 16th, 2026

@Jarred-Sumner, your commit 566cb13 has 1 failures in Build #63053 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32437

That installs a local version of the PR into your bun-32437 executable, so you can run:

bun-32437 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Broken imports #30651 - Reports the same "Cannot access before initialization" TDZ error from parallel dynamic imports reaching a shared TLA dependency, matching the exact deadlock pattern this PR fixes

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #30651

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2c38bc5b-0141-4567-b6ce-8a76ffcb7e62

📥 Commits

Reviewing files that changed from the base of the PR and between 758bc34 and cdd26eb.

📒 Files selected for processing (3)
  • scripts/build/deps/webkit.ts
  • src/jsc/bindings/ZigGlobalObject.cpp
  • test/js/bun/resolve/dynamic-import-tla-cycle.test.ts

Walkthrough

moduleLoaderImportModule is refactored to compute a single normalized sourceOriginStringHolder covering all URL protocol cases and derive referrerAsyncOrder via asyncEvaluationOrderForKey for file origins. Both virtual and non-virtual import paths now receive the normalized origin and forward referrerAsyncOrder with deferred=false to JSC::importModule. A regression test covering sibling dynamic imports sharing a TLA wrapper is added. The WebKit prebuilt version is also updated to a new commit hash.

Changes

TLA sibling dynamic-import fix and regression test

Layer / File(s) Summary
moduleLoaderImportModule: normalized source origin and referrerAsyncOrder forwarding
src/jsc/bindings/ZigGlobalObject.cpp
Normalizes sourceOrigin.url() into sourceOriginStringHolder for all protocol cases (empty, file, builtin, other); computes referrerAsyncOrder via asyncEvaluationOrderForKey for file origins; passes normalized origin to virtual-module resolution; removes the old non-virtual sourceURL extraction and the duplicate sourceOriginStringHolder block; forwards referrerAsyncOrder with deferred=false to both virtual and non-virtual JSC::importModule calls.
TLA sibling dynamic-import regression test
test/js/bun/resolve/dynamic-import-tla-cycle.test.ts
Adds a test case with an entry module concurrently importing two consumers that share a TLA wrapper module; the wrapper awaits a dynamic import of an inner module and re-exports its bindings. Asserts stdout is foo bar, stderr is empty, and process exits successfully.

WebKit prebuilt version update

Layer / File(s) Summary
WebKit prebuilt version constant update
scripts/build/deps/webkit.ts
Updates WEBKIT_VERSION constant to cd821fecca0d39c8bac874c283d956868c7f0de0, which changes the prebuilt artifact tag/identity used when cfg.webkit is set to prebuilt.

Possibly related issues

Possibly related PRs

  • oven-sh/bun#31169: Both PRs modify Zig::GlobalObject::moduleLoaderImportModule at the same import hook, with overlapping changes to the deferred parameter introduction and source-origin handling.
  • oven-sh/bun#32414: Both PRs update the WEBKIT_VERSION constant in scripts/build/deps/webkit.ts for WebKit prebuilt selection.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: threading referrer asyncEvaluationOrder into moduleLoaderImportModule to address TLA self-deadlock issues.
Description check ✅ Passed The description covers both required sections: 'What' explains the technical implementation including the referrer asyncEvaluationOrder lookup and path computation hoisting, and 'How did you verify' discusses the test coverage and existing test validation.
Linked Issues check ✅ Passed The PR directly addresses issue #30634 by implementing the necessary module loader changes to fix ESM TDZ errors in concurrent sibling imports with TLA re-exports, including test coverage for the exact scenario reported.
Out of Scope Changes check ✅ Passed All changes are in-scope: ZigGlobalObject.cpp implements the fix, the test validates #30634, and webkit.ts version bump is necessary for the fix to function correctly.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jsc/bindings/ZigGlobalObject.cpp`:
- Around line 3482-3484: The issue is that the asyncEvaluationOrderForKey()
lookup uses sourceOriginStringHolder which is derived from
sourceURL.fileSystemPath(), but fileSystemPath() strips query strings. However,
modules are registered in the loader with their full identifiers including query
strings, so the lookup fails to find the cached entry. Fix this by extracting
the search component from sourceURL (the query string portion) and appending it
to sourceOriginStringHolder before passing it to asyncEvaluationOrderForKey(),
matching the pattern used for resolvedIdentifier on lines 3540-3544 where
queryString is appended. Additionally, add a test case that imports a module
with a query parameter to verify the async evaluation ordering optimization
works correctly for referrers with query strings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0b6543a0-daff-4d3a-9ec5-f5be26ea2983

📥 Commits

Reviewing files that changed from the base of the PR and between 91ce3b4 and 7cddf39.

📒 Files selected for processing (4)
  • scripts/build/deps/webkit.ts
  • src/jsc/bindings/NodeVM.cpp
  • src/jsc/bindings/ZigGlobalObject.cpp
  • test/js/bun/resolve/dynamic-import-tla-cycle.test.ts

Comment thread src/jsc/bindings/ZigGlobalObject.cpp Outdated
Comment on lines +3484 to +3488
auto query = sourceURL.queryWithLeadingQuestionMark();
auto referrerKey = query.isEmpty()
? JSC::Identifier::fromString(vm, sourceOriginStringHolder)
: JSC::Identifier::fromString(vm, makeString(sourceOriginStringHolder, query));
referrerAsyncOrder = globalObject->moduleLoader()->asyncEvaluationOrderForKey(referrerKey);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The !query.isEmpty() branch added in bfdabd0 is unreachable: Bun's SourceOrigin for file modules is built from ResolvedSource.source_url, which is always set to path.text (the query-stripped filesystem path) and then run through WTF::URL::fileURLWithFileSystemPath(), so sourceURL.queryWithLeadingQuestionMark() is always empty. A referrer registered as /abs/wrapper.mjs?v=1 is therefore still looked up as /abs/wrapper.mjs, asyncEvaluationOrderForKey returns -1, and the #30634 fix doesn't apply to query-keyed TLA referrers — the CodeRabbit comment marked '✅ Addressed' isn't actually addressed. Not a regression (the no-query #30634 case is fixed), but consider either threading the query into ResolvedSource.source_url / the SourceOrigin, or dropping the dead branch.

Extended reasoning...

What the bug is

Commit bfdabd0 was added in response to the CodeRabbit inline comment: when the referrer module's registry key includes a query string (e.g. /abs/wrapper.mjs?v=1), the asyncEvaluationOrderForKey() lookup should include that query so it matches the registry entry. The fix reads sourceURL.queryWithLeadingQuestionMark() and, if non-empty, appends it to the filesystem path before the lookup.

The problem is that for file-protocol referrers in Bun, sourceURL never has a query component, so query.isEmpty() is always true and the makeString(...) branch is dead code. The CodeRabbit comment is marked "✅ Addressed in commit bfdabd0", but the query-keyed-referrer case it describes remains unfixed.

The code path

sourceURL here is sourceOrigin.url(). For file modules the SourceOrigin is constructed in ZigSourceProvider.cpp:

// ZigSourceProvider.cpp:89
auto sourceURLString = resolvedSource.source_url.toWTFString(BunString::ZeroCopy);
// ZigSourceProvider.cpp:48 (via toSourceOrigin)
return SourceOrigin(WTF::URL::fileURLWithFileSystemPath(sourceURL));

Every assignment of ResolvedSource.source_url in the loaders — ModuleLoader.zig:109,348,362,371,380,392,410,445,588,..., RuntimeTranspilerStore.rs:556, AsyncModule.zig:731, VirtualMachine.zig:1596,1610 — sets it to the content of path.text via input_specifier.createIfDifferent(path.text) (or String.init(path.text)). createIfDifferent (string.zig:117-125) returns other.dupeRef() when other equals utf8_slice, else cloneUTF8(utf8_slice) — i.e. its result is always semantically equal to the second argument, path.text.

path.text is the resolved on-disk path. The query was already split off by normalizeSpecifierForResolution (VirtualMachine.zig:1712-1721) / normalizeSpecifier (options.zig:935-966) before Fs.Path.init, and is never re-joined into path.text. So source_url is always the query-less filesystem path. (And even if a ? survived, fileURLWithFileSystemPath() percent-encodes it into the path component, so the resulting WTF::URL would still have an empty query.)

Why existing code doesn't prevent it

Registry keys do include the query: the same function builds resolvedIdentifier = makeString(resolved.result.value, queryString) at line 3547, so a module imported as ./wrapper.mjs?v=1 lives in the loader registry under /abs/wrapper.mjs?v=1. But the referrer lookup key is derived from sourceOrigin.url(), which — as shown above — never carries the query. CodeRabbit's premise ("that query is present in sourceURL") is wrong for Bun's file modules; bfdabd0 implemented exactly what it suggested, so it inherits the wrong premise.

Step-by-step proof

Given:

entry.mjs:     await Promise.all([import('./consumer1.mjs?v=1'), import('./consumer2.mjs?v=1')])
consumerN.mjs: import { X } from './wrapper.mjs?v=1'
wrapper.mjs:   const m = await import('./inner.mjs'); export const X = m.X;
  1. consumer1.mjs?v=1 resolves ./wrapper.mjs?v=1 → registry key /abs/wrapper.mjs?v=1 (line 3547 path).
  2. The fetch for that key transpiles /abs/wrapper.mjs; ResolvedSource.source_url = path.text = "/abs/wrapper.mjs" (no query).
  3. ZigSourceProvider builds SourceOrigin(fileURLWithFileSystemPath("/abs/wrapper.mjs"))file:///abs/wrapper.mjs.
  4. wrapper.mjs runs await import('./inner.mjs'); moduleLoaderImportModule receives sourceOrigin.url() = file:///abs/wrapper.mjs.
  5. Line 3484: queryWithLeadingQuestionMark()""; referrerKey = "/abs/wrapper.mjs".
  6. asyncEvaluationOrderForKey("/abs/wrapper.mjs") misses (registry has /abs/wrapper.mjs?v=1) → returns -1.
  7. referrerAsyncOrder = -1 is forwarded to JSC::importModule, so the TLA self-deadlock skip never fires for this referrer — the [1.3.14] ESM TDZ error when importing Lexical React modules that re-export through top-level await #30634 TDZ behaviour persists for query-keyed wrappers.

Impact

Not a regression: pre-PR there was no referrerAsyncOrder at all, so query-keyed referrers were equally broken. The no-query case — which is what #30634 actually reports and what the new test covers — is fixed. The impact is (a) misleading dead code, and (b) the "✅ Addressed" mark on the review thread is inaccurate: the query-stringed-referrer edge case remains unfixed.

How to fix

Either:

  • Thread the query into the SourceOrigin so sourceURL actually carries it — e.g. set ResolvedSource.source_url to the full registry key (path + query) instead of bare path.text, or append the query before calling fileURLWithFileSystemPath and re-set it on the resulting URL; or
  • Drop the dead !query.isEmpty() branch and leave a comment that query-keyed referrers aren't yet handled, so the code doesn't imply otherwise.

If you keep the fix, a variant of the new test that imports ./wrapper.mjs?v=1 would exercise it.

…importModule

For the TLA self-deadlock skip at innerModuleEvaluation 12.b.v (see
oven-sh/WebKit claude/tla-referrer-async-order). Look up the referrer's
asyncEvaluationOrder via the new JSModuleLoader::asyncEvaluationOrderForKey()
and pass it through; the resolve() referrer stays empty so plugin onResolve and
the second resolve pass are unchanged.

Hoists the sourceOrigin->path computation so the existing virtual-module branch
and the resolve block share it.

Adds test for #30634 (sibling dynamic imports sharing a TLA wrapper).
…ey lookup

A referrer registered as /abs/index.mjs?v=1 has that as its module-map key;
sourceURL.fileSystemPath() drops the query, so the asyncEvaluationOrder lookup
missed and the deadlock skip never fired for query-keyed TLA modules.
@Jarred-Sumner
Jarred-Sumner force-pushed the claude/tla-referrer-async-order branch from 758bc34 to cdd26eb Compare June 17, 2026 01:05
@Jarred-Sumner
Jarred-Sumner merged commit 0c537fe into main Jun 17, 2026
77 of 78 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the claude/tla-referrer-async-order branch June 17, 2026 04:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[1.3.14] ESM TDZ error when importing Lexical React modules that re-export through top-level await

2 participants