@@ -5,8 +5,9 @@ use itertools::Itertools;
55use oxc_index:: IndexVec ;
66use rolldown_common:: {
77 Chunk , ChunkDebugInfo , ChunkIdx , ChunkKind , ChunkMeta , ChunkReasonType ,
8- FacadeChunkEliminationReason , Module , ModuleIdx , ModuleNamespaceIncludedReason , ModuleTable ,
9- PostChunkOptimizationOperation , PreserveEntrySignatures , RuntimeHelper , StmtInfos , WrapKind ,
8+ FacadeChunkEliminationReason , IdxDebugExt , Module , ModuleIdx , ModuleNamespaceIncludedReason ,
9+ ModuleTable , PostChunkOptimizationOperation , PreserveEntrySignatures , RuntimeHelper , StmtInfos ,
10+ WrapKind ,
1011} ;
1112use rolldown_utils:: { BitSet , indexmap:: FxIndexMap } ;
1213use rustc_hash:: { FxHashMap , FxHashSet } ;
@@ -22,8 +23,10 @@ use crate::{
2223} ;
2324
2425use super :: {
25- GenerateStage , chunk_ext:: ChunkCreationReason , chunk_ext:: ChunkDebugExt ,
26- code_splitting:: IndexSplittingInfo ,
26+ GenerateStage ,
27+ chunk_ext:: ChunkCreationReason ,
28+ chunk_ext:: ChunkDebugExt ,
29+ code_splitting:: { IndexSplittingInfo , PendingCommonChunkInfo } ,
2730} ;
2831
2932/// Information about a facade chunk merge operation.
@@ -93,16 +96,40 @@ impl GenerateStage<'_> {
9396 chunk_graph : & mut ChunkGraph ,
9497 bits_to_chunk : & mut FxHashMap < BitSet , ChunkIdx > ,
9598 input_base : & ArcStr ,
96- pending_common_chunks : FxIndexMap < BitSet , Vec < ModuleIdx > > ,
99+ pending_common_chunks : FxIndexMap < BitSet , PendingCommonChunkInfo > ,
97100 ) {
98101 let static_entry_chunk_reference: FxHashMap < ChunkIdx , FxHashSet < ChunkIdx > > =
99102 self . construct_static_entry_to_reached_dynamic_entries_map ( chunk_graph) ;
100103
101104 let entry_chunk_idx =
102105 chunk_graph. chunk_table . iter_enumerated ( ) . map ( |( idx, _) | idx) . collect :: < FxHashSet < _ > > ( ) ;
106+ // Calculate on demand to avoid add a new field on each NormalModule.
107+ let dynamic_entry_to_dynamic_importers: FxHashMap < ModuleIdx , FxHashSet < ModuleIdx > > = {
108+ // Get dynamic entry modules from chunk_table, then find matched entry points
109+ // and extract importers from related_stmt_infos
110+ let mut map: FxHashMap < ModuleIdx , FxHashSet < ModuleIdx > > = FxHashMap :: default ( ) ;
111+ for chunk in chunk_graph. chunk_table . iter ( ) {
112+ let ChunkKind :: EntryPoint { meta, module, .. } = chunk. kind else {
113+ continue ;
114+ } ;
115+ if meta != ChunkMeta :: DynamicImported {
116+ continue ;
117+ }
118+ // Find the matched dynamic entry point from link_output.entries
119+ if let Some ( entries) = self . link_output . entries . get ( & module) {
120+ for entry in entries. iter ( ) . filter ( |e| e. kind . is_dynamic_import ( ) ) {
121+ // Extract importers from related_stmt_infos (first element is the importer ModuleIdx)
122+ for ( importer_idx, _, _, _) in & entry. related_stmt_infos {
123+ map. entry ( module) . or_default ( ) . insert ( * importer_idx) ;
124+ }
125+ }
126+ }
127+ }
128+ map
129+ } ;
103130 // extract entry chunk module relation
104131 // this means `key_chunk` also referenced all entry module in value `vec`
105- for ( bits, modules) in pending_common_chunks {
132+ for ( bits, PendingCommonChunkInfo { modules, meta : _ } ) in pending_common_chunks {
106133 let chunk_idxs = bits
107134 . index_of_one ( )
108135 . into_iter ( )
@@ -117,6 +144,8 @@ impl GenerateStage<'_> {
117144 & static_entry_chunk_reference,
118145 chunk_graph,
119146 & self . link_output . module_table ,
147+ & dynamic_entry_to_dynamic_importers,
148+ & modules,
120149 ) ;
121150
122151 self . assign_modules_to_chunk (
@@ -238,6 +267,8 @@ impl GenerateStage<'_> {
238267 entry_chunk_reference : & FxHashMap < ChunkIdx , FxHashSet < ChunkIdx > > ,
239268 chunk_graph : & ChunkGraph ,
240269 module_table : & ModuleTable ,
270+ dynamic_entry_to_dynamic_importers : & FxHashMap < ModuleIdx , FxHashSet < ModuleIdx > > ,
271+ modules : & [ ModuleIdx ] ,
241272 ) -> Option < ChunkIdx > {
242273 let mut user_defined_entry = vec ! [ ] ;
243274 let mut dynamic_entry = vec ! [ ] ;
@@ -262,18 +293,33 @@ impl GenerateStage<'_> {
262293 Self :: find_merge_target ( & user_defined_entry, & user_defined_entry_modules, module_table) ;
263294 if !user_defined_entry. is_empty ( ) {
264295 let chunk_idx = merged_user_defined_chunk?;
265-
266- let ret = dynamic_entry. iter ( ) . all ( |idx| {
267- entry_chunk_reference
268- . get ( & chunk_idx)
269- . map ( |reached_dynamic_chunk| reached_dynamic_chunk. contains ( idx) )
270- . unwrap_or ( false )
296+ let chunk = & chunk_graph. chunk_table [ chunk_idx] ;
297+ let reached_dynamic_chunk = entry_chunk_reference. get ( & chunk_idx) ;
298+ // Check if all dynamic entry chunks are reachable from the merged user-defined entry chunk.
299+ // If not, we cannot merge the shared modules into the user-defined entry chunk because
300+ // the dynamic entries would not be able to access the shared modules.
301+ let all_dynamic_entries_reachable =
302+ dynamic_entry. iter ( ) . all ( |idx| reached_dynamic_chunk. is_some_and ( |set| set. contains ( idx) ) ) ;
303+ if !all_dynamic_entries_reachable {
304+ return None ;
305+ }
306+ let merged_entry_module = chunk. entry_module ( module_table) ?;
307+ // For each module in the shared modules, if it is a dynamic entry module,
308+ // all its dynamic importers must also statically import the entry module of the merged chunk.
309+ // This ensures that when a dynamic import occurs, the merged entry chunk (containing the
310+ // dynamic entry module) is already loaded, preventing missing module errors at runtime.
311+ let all_dynamic_entry_importers_valid = modules. iter ( ) . all ( |& module_idx| {
312+ let Some ( importers) = dynamic_entry_to_dynamic_importers. get ( & module_idx) else {
313+ // Not a dynamic entry module, no constraint
314+ return true ;
315+ } ;
316+ importers. iter ( ) . all ( |importer| * importer == merged_entry_module. idx )
271317 } ) ;
272- return ret. then_some ( chunk_idx) ;
318+ all_dynamic_entry_importers_valid. then_some ( chunk_idx)
319+ } else {
320+ let dynamic_chunk_entry_modules = Self :: collect_entry_modules ( & dynamic_entry, chunk_graph) ?;
321+ Self :: find_merge_target ( & dynamic_entry, & dynamic_chunk_entry_modules, module_table)
273322 }
274-
275- let dynamic_chunk_entry_modules = Self :: collect_entry_modules ( & dynamic_entry, chunk_graph) ?;
276- Self :: find_merge_target ( & dynamic_entry, & dynamic_chunk_entry_modules, module_table)
277323 }
278324
279325 /// Collects entry module indices from a list of chunk indices.
@@ -350,6 +396,7 @@ impl GenerateStage<'_> {
350396 /// Finds empty dynamic entry chunks that should be merged with their target common chunks.
351397 /// Returns a tuple of (merge_entry_to_chunk, emitted_chunk_groups).
352398 fn find_facade_chunk_merge_candidates (
399+ & self ,
353400 chunk_graph : & ChunkGraph ,
354401 ) -> ( FxHashMap < ModuleIdx , FacadeChunkMergeInfo > , FxHashMap < ChunkIdx , Vec < ChunkIdx > > ) {
355402 let mut merge_entry_to_chunk = FxHashMap :: default ( ) ;
@@ -385,6 +432,9 @@ impl GenerateStage<'_> {
385432 ChunkReasonType :: ManualCodeSplitting { .. }
386433 ) ;
387434 let is_pure_user_defined_to_entry_chunk = matches ! ( target_chunk. kind, ChunkKind :: EntryPoint { meta, bit: _, module: _ } if meta. is_pure_user_defined_entry( ) ) ;
435+ dbg ! ( & chunk_idx. debug( & self . link_output. module_table, & chunk_graph. chunk_table) ) ;
436+ dbg ! ( & target_chunk_idx. debug( & self . link_output. module_table, & chunk_graph. chunk_table) ) ;
437+ dbg ! ( & target_chunk. kind) ;
388438 // Three optimization scenarios:
389439 // 1. Emitted chunk (AllowExtension) merged into manual code splitting group
390440 // → Group by target chunk to detect export name conflicts before merging
@@ -477,7 +527,7 @@ impl GenerateStage<'_> {
477527 ) {
478528 // Find empty dynamic entry chunks that should be merged with their target common chunks
479529 let ( mut merge_entry_to_chunk, emitted_chunk_groups) =
480- Self :: find_facade_chunk_merge_candidates ( chunk_graph) ;
530+ self . find_facade_chunk_merge_candidates ( chunk_graph) ;
481531
482532 if merge_entry_to_chunk. is_empty ( ) && emitted_chunk_groups. is_empty ( ) {
483533 return ;
0 commit comments