Skip to content

fix(esm_library): extract TLA shared modules to break circular dependency#13606

Merged
JSerFeng merged 13 commits intomainfrom
fy/cranky-chaplygin
Apr 17, 2026
Merged

fix(esm_library): extract TLA shared modules to break circular dependency#13606
JSerFeng merged 13 commits intomainfrom
fy/cranky-chaplygin

Conversation

@JSerFeng
Copy link
Copy Markdown
Contributor

@JSerFeng JSerFeng commented Apr 3, 2026

Summary

  • When using ESM library output with top-level await (TLA), a circular dependency occurs: the main chunk await import()s the async chunk (because it has TLA), but the async chunk imports shared modules back from the main chunk via the "available modules" optimization.
  • Adds extract_tla_shared_modules() in the optimize_chunks hook that scans async chunks' dependency graphs and extracts modules residing in ancestor chunks into separate chunks to break the cycle.
  • Emits a warning when TLA is used with ESM library output, since bundled execution order may differ from the original source.

Link

web-infra-dev/rslib#1573

Test plan

  • cargo check -p rspack_plugin_esm_library passes
  • cargo test -p rspack_plugin_esm_library — all 19 tests pass
  • Manual verification with a TLA + shared dependency scenario

Copilot AI review requested due to automatic review settings April 3, 2026 10:11
@github-actions github-actions Bot added team The issue/pr is created by the member of Rspack. release: bug fix release: bug related release(mr only) labels Apr 3, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75e0197b0c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/rspack_plugin_esm_library/src/optimize_chunks.rs Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses a circular dependency that can occur in ESM library output when top-level await (TLA) causes an async chunk to be awaited by an ancestor chunk while also importing shared modules back from that ancestor. It adds a chunk-graph optimization pass to extract such shared modules into split chunks (breaking the cycle) and emits a warning when TLA is detected in ESM library output.

Changes:

  • Add extract_tla_shared_modules() to scan async chunks and extract ancestor-resident dependencies into split chunks.
  • Invoke the new extraction pass during CompilationOptimizeChunks and emit a TLA warning diagnostic.
  • Extend optimize-chunks utilities with dependency-graph traversal for identifying problematic cross-chunk dependencies.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
crates/rspack_plugin_esm_library/src/plugin.rs Calls the new extraction pass during optimize-chunks and emits a TLA warning diagnostic.
crates/rspack_plugin_esm_library/src/optimize_chunks.rs Implements extract_tla_shared_modules() to detect and extract shared modules from ancestor chunks to break TLA-induced cycles.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/rspack_plugin_esm_library/src/optimize_chunks.rs
Comment thread crates/rspack_plugin_esm_library/src/optimize_chunks.rs
Comment thread crates/rspack_plugin_esm_library/src/optimize_chunks.rs Outdated
Comment thread crates/rspack_plugin_esm_library/src/optimize_chunks.rs Outdated
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 3, 2026

Merging this PR will degrade performance by 1.29%

⚡ 1 improved benchmark
❌ 2 regressed benchmarks
✅ 25 untouched benchmarks

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation rust@create_chunk_ids 10.6 ms 10.3 ms +2.82%
Simulation rust@create_module_hashes 21.4 ms 21.6 ms -1.07%
Simulation rust@persistent_cache_restore@basic-react-development 25.7 ms 26 ms -1.29%

Comparing fy/cranky-chaplygin (67ca083) with main (3d76b2b)

Open in CodSpeed

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 3, 2026

Rsdoctor Bundle Diff Analysis

Found 6 projects in monorepo, 0 projects with changes.

📊 Quick Summary
Project Total Size Change
popular-libs 1.7 MB 0
react-10k 5.7 MB 0
react-1k 826.1 KB 0
react-5k 2.7 MB 0
rome 984.1 KB 0
ui-components 5.0 MB 0

Generated by Rsdoctor GitHub Action

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 3, 2026

📦 Binary Size-limit

Comparing 67ca083 to fix(esm-library): deconflict module external helper names (#13695) by Fy

❌ Size increased by 16.16KB from 49.39MB to 49.41MB (⬆️0.03%)

@JSerFeng
Copy link
Copy Markdown
Contributor Author

JSerFeng commented Apr 7, 2026

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0f9d9dbec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/rspack_plugin_esm_library/src/optimize_chunks.rs Outdated
@JSerFeng JSerFeng force-pushed the fy/cranky-chaplygin branch from cc11129 to 9f1b14a Compare April 13, 2026 03:09
@JSerFeng
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41336613cc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/rspack_plugin_esm_library/src/optimize_chunks.rs Outdated
Comment thread crates/rspack_plugin_esm_library/src/optimize_chunks.rs
JSerFeng added 12 commits April 15, 2026 20:03
…ency

When using ESM library output with top-level await (TLA), a circular
dependency occurs: the main chunk awaits the async chunk (because it has
TLA), but the async chunk imports shared modules from the main chunk.

This adds `extract_tla_shared_modules()` which scans async chunks'
dependency graphs and extracts modules that reside in ancestor chunks
into separate chunks to break the cycle.

Also emits a warning when TLA is used with ESM library output, as the
bundled execution order may differ from the original source.
- Reconnect entry modules to new chunk with entrypoint metadata after
  split, preventing entrypoints from losing their startup module
- Group extracted modules by their source chunk set to avoid duplicating
  a module into multiple new chunks when it appears in multiple ancestors
- Filter inactive connections via is_target_active() during BFS to avoid
  over-approximating the dependency graph
- Fix doc comment: clarify that early exit skips chunk-graph scanning,
  not all computation
- Fix cargo fmt: merge std imports, fix line break
- Change extract_tla_shared_modules to return true only when modules
  were actually extracted, not just when TLA exists
- Only emit the warning diagnostic when a circular dependency is found
  and shared modules are extracted, avoiding false warnings in tests
  that legitimately use TLA
- Remove redundant has_tla early exit since async_chunks.is_empty()
  already covers the no-TLA case
- Use is_only_initial instead of can_be_initial to also catch chunks
  that belong to both initial and non-initial groups
Replace per-async-chunk independent BFS with a single BFS pass sharing
one visited set across all async chunks. Pre-compute a reverse map
(chunk → which async chunks consider it an ancestor) to enable O(1)
ancestor lookups during traversal.

Complexity: O(N + E + A × G) down from O(A × (N + E + G))
where N=modules, E=edges, A=async chunks, G=chunk groups.
Remove test.filter.js from split-sync-modules-{1,2,3} tests that were
skipped with "cycle caused by available modules". These tests exercise
the exact circular dependency scenario that extract_tla_shared_modules
now fixes.

Add warnings.js to expect the TLA circular dependency warning that is
emitted when shared modules are extracted.
- Fix clippy collapsible_if lint by merging nested if-let + if
- Restore test.filter.js for split-sync-modules tests: the chunk
  extraction alone is not sufficient to break the cycle in ESM
  rendering — additional render-side changes are needed
- Add side_effects_state_artifact param for is_target_active API change
Remove test.filter.js so the tests actually execute, and add warnings.js
to expect the TLA circular dependency warning.
Previously we detected "async chunks" as non-initial chunks containing
a module marked async. This missed the actual cycle scenario: the async
chunk's modules themselves are sync; what matters is that the importing
module has TLA and uses await-import to load the chunk.

New detection: scan all modules with has_top_level_await, follow their
DynamicImport dependencies, and mark the non-initial target chunks as
potentially problematic. This correctly covers the
split-sync-modules test cases where the imported chunk re-exports from
a lib that sits in the parent chunk due to available-modules dedup.
Previously the extraction reconnected ALL moved modules as entry modules
to every initial entrypoint group of the new chunk. This was wrong —
only modules that were ORIGINALLY entry modules in their source chunks
should be reconnected, and only to their original entrypoint group.
Wrongly promoting shared library modules (like lib.js) to entrypoint
status corrupted the chunk graph and caused runtime deadlocks.

Key changes:
- Only reconnect modules as entry modules if they WERE entry modules in
  some source chunk (tracked per-source-chunk via
  get_chunk_entry_modules_with_chunk_group_iterable lookup before
  disconnecting)
- Use the ORIGINAL entrypoint group when reconnecting, not "all initial
  groups of the new chunk"
- Skip DynamicImport dependencies during BFS — they produce promises
  and don't force the target chunk to be evaluated synchronously, so
  they can't cause the TLA deadlock
- Set chunk_reason for debuggability

This mirrors the pattern used by RemoveDuplicateModulesPlugin.
Run with UPDATE_SNAPSHOT=true locally confirms all 3 tests pass:
lib.js is extracted into a separate chunk, main.mjs and async.mjs
both import from it — no circular dependency.

All 198 ESM output tests pass, no regressions.
Previously the BFS used a shared visited set with single-origin tracking
per module. This was buggy when a module was reachable from multiple
async chunks whose ancestors differ:

  - Module M lives in chunks [P1, P2]
  - P1 is ancestor of async chunk A1, P2 is ancestor of A2
  - A1's BFS reaches M first, records extract from P1
  - A2's BFS skips M via visited, never records extract from P2
  - Result: cycle between A2 and P2 remains

Fix: run an independent BFS per async chunk with its own visited set.
Each module is analyzed in the context of the async chunk actually
traversing it, so extractions for different ancestor→async edges don't
collide. Complexity goes from O(N+E+A·G) back to O(A·(N+E+G)), but
correctness trumps the marginal speedup — A is usually small.

Also add a note about the Phase 1 over-inclusion of function-scoped
import()s; accepted as safe because extraction only fires when real
cross-chunk static cycles exist.
@JSerFeng JSerFeng force-pushed the fy/cranky-chaplygin branch from 04ed988 to a4bf205 Compare April 15, 2026 12:06
@JSerFeng
Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to set up container
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@JSerFeng JSerFeng enabled auto-merge (squash) April 16, 2026 02:50
@JSerFeng
Copy link
Copy Markdown
Contributor Author

@codex review

1 similar comment
@JSerFeng
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a4bf205133

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/rspack_plugin_esm_library/src/optimize_chunks.rs Outdated
When an ancestor module (sharedA) itself imports another module
(sharedB) also in an ancestor chunk, the BFS must continue into
sharedA to discover sharedB. Previously the BFS stopped at ancestor
boundaries, only extracting direct dependencies but leaving their
transitive deps in the parent chunk — preserving the cycle.

Now the BFS continues into both in-async-chunk targets AND
ancestor-resident targets, ensuring the full transitive closure of
static dependencies is extracted.
@JSerFeng JSerFeng merged commit b962256 into main Apr 17, 2026
74 of 78 checks passed
@JSerFeng JSerFeng deleted the fy/cranky-chaplygin branch April 17, 2026 03:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release: bug fix release: bug related release(mr only) team The issue/pr is created by the member of Rspack.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants