Skip to content

Commit 7da4bdc

Browse files
authored
fix: when there are 2+ dep targets in the same chunk, create facade chunks for all of them (#13154)
fix: when there are 2+ targets in the same chunk, create facade chunks for all of them
1 parent fe401a7 commit 7da4bdc

46 files changed

Lines changed: 549 additions & 2 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/rspack_plugin_esm_library/src/optimize_chunks.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use rayon::prelude::*;
22
use rspack_collections::{IdentifierMap, IdentifierSet, UkeyDashSet, UkeyMap, UkeySet};
33
use rspack_core::{
44
ChunkGroupUkey, ChunkUkey, Compilation, DependenciesBlock, DependencyType, Logger,
5-
incremental::Mutation,
5+
ModuleIdentifier, incremental::Mutation,
66
};
77

88
use crate::EsmLibraryPlugin;
@@ -383,6 +383,10 @@ pub(crate) fn ensure_dyn_import_namespace_facades(
383383
}
384384
}
385385

386+
// Track how many dyn targets are in each multi-module chunk.
387+
// When multiple dyn targets share a chunk, we need facades to avoid export conflicts.
388+
let mut dyn_targets_per_chunk = UkeyMap::<ChunkUkey, Vec<ModuleIdentifier>>::default();
389+
386390
for module_id in &all_dyn_targets {
387391
let Some(module) = module_graph.module_by_identifier(module_id) else {
388392
continue;
@@ -415,8 +419,25 @@ pub(crate) fn ensure_dyn_import_namespace_facades(
415419
} else {
416420
already_strict.insert(chunk_ukey);
417421
}
422+
} else if concatenated_modules.contains(module_id) {
423+
// Multi-module chunk with only named imports: track for potential conflict detection.
424+
dyn_targets_per_chunk
425+
.entry(chunk_ukey)
426+
.or_default()
427+
.push(*module_id);
428+
}
429+
}
430+
431+
// When multiple dyn targets share the same multi-module chunk, their exports
432+
// would be merged into the chunk's exports. If they have overlapping export names,
433+
// this causes conflicts. Create facade chunks for all of them to ensure each
434+
// dynamic import gets the correct module's exports.
435+
for (chunk_ukey, targets) in &dyn_targets_per_chunk {
436+
if targets.len() > 1 {
437+
for module_id in targets {
438+
needs_split.push((*module_id, *chunk_ukey));
439+
}
418440
}
419-
// Multi-module chunk with only named imports: .then() remapping handles it, no split needed.
420441
}
421442

422443
needs_split.sort_by_key(|(module_id, _)| *module_id);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
```mjs title=0.mjs
2+
3+
export { value } from "./ab-chunk.mjs";
4+
5+
```
6+
7+
```mjs title=1.mjs
8+
9+
export { value_0 as value } from "./ab-chunk.mjs";
10+
11+
```
12+
13+
```mjs title=ab-chunk.mjs
14+
// ./a.js
15+
const value = 1
16+
17+
// ./b.js
18+
const b_value = 2
19+
20+
export { b_value as value_0, value };
21+
22+
```
23+
24+
```mjs title=main.mjs
25+
import { value, value_0 as b_value } from "./ab-chunk.mjs";
26+
27+
// ./index.js
28+
29+
30+
31+
it('should handle conflicting exports from multi-module chunk', async () => {
32+
expect(value).toBe(1)
33+
expect(b_value).toBe(2)
34+
35+
const modA = await import("./0.mjs")
36+
const modB = await import("./1.mjs")
37+
38+
expect(modA.value).toBe(1)
39+
expect(modB.value).toBe(2)
40+
})
41+
42+
43+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const value = 1
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const value = 2
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { value as aValue } from './a'
2+
import { value as bValue } from './b'
3+
4+
it('should handle conflicting exports from multi-module chunk', async () => {
5+
expect(aValue).toBe(1)
6+
expect(bValue).toBe(2)
7+
8+
const modA = await import('./a')
9+
const modB = await import('./b')
10+
11+
expect(modA.value).toBe(1)
12+
expect(modB.value).toBe(2)
13+
})
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = {
2+
optimization: {
3+
splitChunks: {
4+
cacheGroups: {
5+
ab: {
6+
test: /[ab]\.js$/,
7+
name: "ab-chunk",
8+
chunks: "all",
9+
}
10+
}
11+
}
12+
}
13+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
```mjs title=main.mjs
2+
// ./index.js
3+
it('should dynamically import module with default interop from external', async () => {
4+
const mod = await import("./wrapper_js.mjs")
5+
expect(mod.default).toBeDefined()
6+
expect(mod.readFile).toBeDefined()
7+
expect(mod.ns).toBeDefined()
8+
})
9+
10+
11+
```
12+
13+
```mjs title=wrapper_js.mjs
14+
import * as __rspack_external_fs from "fs";
15+
16+
// fs
17+
18+
// ./wrapper.js
19+
20+
21+
;
22+
23+
24+
var default_0 = __rspack_external_fs["default"];
25+
var readFile = __rspack_external_fs.readFile;
26+
export { __rspack_external_fs as ns, readFile };
27+
export default default_0;
28+
29+
```
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
it('should dynamically import module with default interop from external', async () => {
2+
const mod = await import('./wrapper')
3+
expect(mod.default).toBeDefined()
4+
expect(mod.readFile).toBeDefined()
5+
expect(mod.ns).toBeDefined()
6+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
externals: {
3+
'fs': 'module fs'
4+
}
5+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { default } from 'fs'
2+
export { readFile } from 'fs'
3+
import * as ns from 'fs'
4+
export { ns }

0 commit comments

Comments
 (0)