Skip to content

fix(esm-lib): mark entry deps exports as used to fix shared chunk splitting#13421

Open
JSerFeng wants to merge 4 commits intoweb-infra-dev:mainfrom
JSerFeng:jserfeng/62fd-demo-rspack-demo
Open

fix(esm-lib): mark entry deps exports as used to fix shared chunk splitting#13421
JSerFeng wants to merge 4 commits intoweb-infra-dev:mainfrom
JSerFeng:jserfeng/62fd-demo-rspack-demo

Conversation

@JSerFeng
Copy link
Contributor

@JSerFeng JSerFeng commented Mar 20, 2026

Summary

  • In finish_modules, mark the direct dependencies of entry modules as "used in unknown way" (all runtimes), so that re-export connections remain active and modules are correctly deduplicated by RemoveDuplicateModulesPlugin.
  • Fix ensure_entry_exports break bug that only marked one dirty entry chunk.
  • Add test case esmOutputCases/split-chunks/multi-entry-shared-reexport.

Root Cause

export * from './a' creates a lazy ESMImportSideEffectDependency when the target module is side-effect-free. The lazy connection's activity depends on per-runtime export usage:

  • entry1 runtime: lib.js's a export is used → lib→a active, lib→b inactive
  • entry2 runtime: lib.js's b export is used → lib→b active, lib→a inactive

During build_chunk_graph, a.js only appears in entry1's chunk and b.js only in entry2's chunk. RemoveDuplicateModulesPlugin only deduplicates modules present in multiple chunks, so a.js/b.js are not extracted. Meanwhile lib.js (present in both) is extracted to a shared chunk — but it references a.js/b.js which remain in their respective entry chunks, creating circular imports.

Fix

Mark direct dependencies of entry modules' exports as used in all runtimes during finish_modules. This keeps re-export connections active across all runtimes, so a.js and b.js appear in both entry chunks and get correctly deduplicated by RemoveDuplicateModulesPlugin into the shared chunk alongside lib.js.

Test plan

  • New test: esmOutputCases/split-chunks/multi-entry-shared-reexport
  • All 89 ESM output tests pass
  • All 10 modern-module config tests pass

…ircular imports

When SplitChunksPlugin extracts shared modules (e.g. lib.js with
`export * from './a'`) into a shared chunk, tree-shaking may leave
the re-exported modules (a.js, b.js) in their respective entry chunks.
This caused the shared chunk to generate circular `import` statements
back to entry chunks.

Add `pull_module_into_non_entry_chunks` which runs before
`ensure_entry_exports` during optimize_chunks. It moves modules from
entry chunks into non-entry initial (shared) chunks when those modules
are referenced by the shared chunk's modules, breaking the cycle.

Also fix a `break` bug in `ensure_entry_exports` that only marked one
entry chunk as dirty when multiple entries were affected.
Verifies that when multiple entries import different exports from a
shared lib module (which uses `export * from`), the re-exported modules
are correctly placed in the shared chunk and each entry imports only
its needed exports without circular chunk dependencies.
…chunks

- Sort chunk keys and module identifiers for deterministic iteration,
  preventing nondeterministic output when multiple shared chunks compete
  for the same module.
- Filter out DynamicImport/DynamicImportEager edges so that lazily
  imported modules are not eagerly pulled into shared chunks.
- Disconnect entry-module mapping when moving a module that is registered
  as an entry module, keeping the chunk graph consistent.
Copilot AI review requested due to automatic review settings March 20, 2026 06:55
@github-actions github-actions bot added the release: bug fix release: bug related release(mr only) label Mar 20, 2026
Copy link

@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: 4eade997b0

ℹ️ 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 on lines +452 to +456
for dep_id in module.get_dependencies() {
if let Some(dep_module) = module_graph.module_identifier_by_dependency_id(dep_id) {
if !entry_modules.contains(dep_module) {
exports_info_artifact
.get_exports_info_data_mut(dep_module)

Choose a reason for hiding this comment

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

P2 Badge Avoid marking every direct entry dependency as fully used

This loop applies set_used_in_unknown_way(None) to every target of module.get_dependencies(), not just re-export edges. In exports_info_setter::set_used_in_unknown_way that upgrades all named exports plus other_exports_info to Unknown, and later link.rs only drops exports whose state is Unused. So an entry like import { foo } from './helper'; export { api } will now keep all of helper's exports live in the emitted ESM-library chunk, even if only foo is needed. That is a broad tree-shaking regression for normal implementation-detail imports; this should be limited to the specific re-export dependencies that need to stay active.

Useful? React with 👍 / 👎.

Copy link
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

Fixes an ESM-library edge case where per-runtime export usage caused re-export connections to be pruned, preventing shared modules from being placed into shared chunks during chunk optimization.

Changes:

  • Mark entry modules and their direct dependencies as “used in unknown way” during finish_modules to keep re-export connections active across runtimes.
  • Fix ensure_entry_exports so it marks all relevant dirty entry chunks (not just the first match).
  • Add a regression test covering multi-entry shared re-exports and the expected ESM output shape.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/rspack-test/esmOutputCases/split-chunks/multi-entry-shared-reexport/side-effect.js Adds a side-effect module used by the shared library to exercise side-effects optimization interplay.
tests/rspack-test/esmOutputCases/split-chunks/multi-entry-shared-reexport/rspack.config.js Defines a multi-entry build (main, entry2) for the regression scenario.
tests/rspack-test/esmOutputCases/split-chunks/multi-entry-shared-reexport/lib.js Shared library that re-exports from a and b, reproducing the per-runtime re-export pruning case.
tests/rspack-test/esmOutputCases/split-chunks/multi-entry-shared-reexport/entry1.js Entry that consumes/export a and asserts runtime behavior via ESM self-import.
tests/rspack-test/esmOutputCases/split-chunks/multi-entry-shared-reexport/entry2.js Second entry that consumes/exports b.
tests/rspack-test/esmOutputCases/split-chunks/multi-entry-shared-reexport/a.js Provides a export for the shared re-export chain.
tests/rspack-test/esmOutputCases/split-chunks/multi-entry-shared-reexport/b.js Provides b export for the shared re-export chain.
tests/rspack-test/esmOutputCases/split-chunks/multi-entry-shared-reexport/snapshots/esm.snap.txt Snapshot asserting the expected chunking + clean ESM re-exports.
crates/rspack_plugin_esm_library/src/plugin.rs Marks entry deps’ exports as used in unknown way to keep re-export connections active across runtimes.
crates/rspack_plugin_esm_library/src/optimize_chunks.rs Removes an early break so all dirty entry chunks are collected correctly.

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

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 20, 2026

Merging this PR will not alter performance

✅ 23 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing JSerFeng:jserfeng/62fd-demo-rspack-demo (c0ad962) with main (6a54e30)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

…itting

Root cause: `export * from './a'` creates a lazy dependency (set_lazy)
when the target module is side-effect-free. The lazy connection's
activity depends on per-runtime export usage — if entry2 doesn't use
export `a`, the lib→a connection is inactive for entry2's runtime.
This causes `a.js` to only appear in entry1's chunk, failing the
minChunks:2 threshold for SplitChunksPlugin extraction.

Fix: in finish_modules, also mark the direct dependencies of entry
modules as "used in unknown way" (all runtimes). This keeps re-export
connections active across all runtimes so modules like a.js/b.js
appear in all entry chunks and get correctly extracted to the shared
chunk by SplitChunksPlugin.

This replaces the previous pull_module_into_non_entry_chunks approach
which moved modules between chunks post-split (treating the symptom
rather than the root cause).
@JSerFeng JSerFeng force-pushed the jserfeng/62fd-demo-rspack-demo branch from 4eade99 to c0ad962 Compare March 20, 2026 07:49
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)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants