rspack_plugin_javascript/plugin/
module_concatenation_plugin.rs

1#![allow(clippy::only_used_in_recursion)]
2use std::{
3  borrow::Cow,
4  collections::{VecDeque, hash_map::DefaultHasher},
5  hash::Hasher,
6  sync::Arc,
7};
8
9use rayon::prelude::*;
10use rspack_collections::{IdentifierDashMap, IdentifierIndexSet, IdentifierMap, IdentifierSet};
11use rspack_core::{
12  ApplyContext, Compilation, CompilationOptimizeChunkModules, CompilerOptions, ExportProvided,
13  ExportsInfoGetter, ExtendedReferencedExport, LibIdentOptions, Logger, Module, ModuleExt,
14  ModuleGraph, ModuleGraphCacheArtifact, ModuleGraphConnection, ModuleGraphModule,
15  ModuleIdentifier, Plugin, PluginContext, PrefetchExportsInfoMode, ProvidedExports,
16  RuntimeCondition, RuntimeSpec, SourceType,
17  concatenated_module::{
18    ConcatenatedInnerModule, ConcatenatedModule, RootModuleContext, is_esm_dep_like,
19  },
20  filter_runtime, get_cached_readable_identifier, get_target,
21  incremental::IncrementalPasses,
22};
23use rspack_error::Result;
24use rspack_hook::{plugin, plugin_hook};
25use rspack_util::itoa;
26use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
27
28fn format_bailout_reason(msg: &str) -> String {
29  format!("ModuleConcatenation bailout: {msg}")
30}
31
32#[derive(Clone, Debug)]
33enum Warning {
34  Id(ModuleIdentifier),
35  Problem(String),
36}
37
38#[derive(Debug, Clone)]
39pub struct ConcatConfiguration {
40  pub root_module: ModuleIdentifier,
41  runtime: Option<RuntimeSpec>,
42  modules: IdentifierIndexSet,
43  warnings: IdentifierMap<Warning>,
44}
45
46impl ConcatConfiguration {
47  pub fn new(root_module: ModuleIdentifier, runtime: Option<RuntimeSpec>) -> Self {
48    let mut modules = IdentifierIndexSet::default();
49    modules.insert(root_module);
50
51    ConcatConfiguration {
52      root_module,
53      runtime,
54      modules,
55      warnings: IdentifierMap::default(),
56    }
57  }
58
59  fn add(&mut self, module: ModuleIdentifier) {
60    self.modules.insert(module);
61  }
62
63  fn has(&self, module: &ModuleIdentifier) -> bool {
64    self.modules.contains(module)
65  }
66
67  fn is_empty(&self) -> bool {
68    self.modules.len() == 1
69  }
70
71  fn add_warning(&mut self, module: ModuleIdentifier, problem: Warning) {
72    self.warnings.insert(module, problem);
73  }
74
75  fn get_warnings_sorted(&self) -> Vec<(ModuleIdentifier, Warning)> {
76    let mut sorted_warnings: Vec<_> = self.warnings.clone().into_iter().collect();
77    sorted_warnings.sort_by_key(|(id, _)| *id);
78    sorted_warnings
79  }
80
81  fn get_modules(&self) -> &IdentifierIndexSet {
82    &self.modules
83  }
84
85  fn snapshot(&self) -> usize {
86    self.modules.len()
87  }
88
89  fn rollback(&mut self, snapshot: usize) {
90    let modules = &mut self.modules;
91    let len = modules.len();
92    for _ in snapshot..len {
93      modules.pop();
94    }
95  }
96}
97
98#[plugin]
99#[derive(Debug, Default)]
100pub struct ModuleConcatenationPlugin {
101  bailout_reason_map: IdentifierDashMap<Arc<Cow<'static, str>>>,
102}
103
104#[derive(Default)]
105pub struct RuntimeIdentifierCache<T> {
106  no_runtime_map: IdentifierMap<T>,
107  runtime_map: HashMap<RuntimeSpec, IdentifierMap<T>>,
108}
109
110impl<T> RuntimeIdentifierCache<T> {
111  fn insert(&mut self, module: ModuleIdentifier, runtime: Option<&RuntimeSpec>, value: T) {
112    if let Some(runtime) = runtime {
113      if let Some(map) = self.runtime_map.get_mut(runtime) {
114        map.insert(module, value);
115      } else {
116        let mut map = IdentifierMap::default();
117        map.insert(module, value);
118        self.runtime_map.insert(runtime.clone(), map);
119      }
120    } else {
121      self.no_runtime_map.insert(module, value);
122    }
123  }
124
125  fn get(&self, module: &ModuleIdentifier, runtime: Option<&RuntimeSpec>) -> Option<&T> {
126    if let Some(runtime) = runtime {
127      let map = self.runtime_map.get(runtime)?;
128
129      map.get(module)
130    } else {
131      self.no_runtime_map.get(module)
132    }
133  }
134}
135
136impl ModuleConcatenationPlugin {
137  fn format_bailout_warning(&self, module: ModuleIdentifier, warning: &Warning) -> String {
138    match warning {
139      Warning::Problem(id) => format_bailout_reason(&format!("Cannot concat with {module}: {id}")),
140      Warning::Id(id) => {
141        let reason = self.get_inner_bailout_reason(id);
142        let reason_with_prefix = match reason {
143          Some(reason) => format!(": {}", *reason),
144          None => "".to_string(),
145        };
146        if id == &module {
147          format_bailout_reason(&format!("Cannot concat with {module}{reason_with_prefix}"))
148        } else {
149          format_bailout_reason(&format!(
150            "Cannot concat with {module} because of {id}{reason_with_prefix}"
151          ))
152        }
153      }
154    }
155  }
156
157  fn set_bailout_reason(
158    &self,
159    module: &ModuleIdentifier,
160    reason: Cow<'static, str>,
161    mg: &mut ModuleGraph,
162  ) {
163    self.set_inner_bailout_reason(module, reason.clone());
164    mg.get_optimization_bailout_mut(module)
165      .push(format_bailout_reason(&reason));
166  }
167
168  fn set_inner_bailout_reason(&self, module: &ModuleIdentifier, reason: Cow<'static, str>) {
169    self.bailout_reason_map.insert(*module, Arc::new(reason));
170  }
171
172  fn get_inner_bailout_reason(
173    &self,
174    module_id: &ModuleIdentifier,
175  ) -> Option<Arc<Cow<'static, str>>> {
176    self
177      .bailout_reason_map
178      .get(module_id)
179      .map(|reason| reason.clone())
180  }
181
182  pub fn get_imports(
183    mg: &ModuleGraph,
184    mg_cache: &ModuleGraphCacheArtifact,
185    mi: ModuleIdentifier,
186    runtime: Option<&RuntimeSpec>,
187    imports_cache: &mut RuntimeIdentifierCache<IdentifierIndexSet>,
188    module_cache: &HashMap<ModuleIdentifier, NoRuntimeModuleCache>,
189  ) -> IdentifierIndexSet {
190    if let Some(set) = imports_cache.get(&mi, runtime) {
191      return set.clone();
192    }
193
194    let cached = module_cache.get(&mi).expect("should have module");
195
196    let mut set = IdentifierIndexSet::default();
197    for (con, (has_imported_names, cached_active)) in &cached.connections {
198      let is_target_active = if let Some(runtime) = runtime {
199        if cached.runtime == *runtime {
200          // runtime is same, use cached value
201          *cached_active
202        } else if cached.runtime.is_subset(runtime) && *cached_active {
203          // cached runtime is subset and active, means it is also active in current runtime
204          true
205        } else if cached.runtime.is_superset(runtime) && !*cached_active {
206          // cached runtime is superset and inactive, means it is also inactive in current runtime
207          false
208        } else {
209          // can't determine, need to check
210          con.is_target_active(mg, Some(runtime), mg_cache)
211        }
212      } else {
213        // no runtime, need to check
214        con.is_target_active(mg, None, mg_cache)
215      };
216
217      if !is_target_active {
218        continue;
219      }
220      if *has_imported_names || cached.provided_names {
221        set.insert(*con.module_identifier());
222      }
223    }
224
225    imports_cache.insert(mi, runtime, set.clone());
226    set
227  }
228
229  #[allow(clippy::too_many_arguments)]
230  fn try_to_add(
231    compilation: &Compilation,
232    config: &mut ConcatConfiguration,
233    module_id: &ModuleIdentifier,
234    runtime: Option<&RuntimeSpec>,
235    active_runtime: Option<&RuntimeSpec>,
236    possible_modules: &IdentifierSet,
237    candidates: &mut IdentifierSet,
238    failure_cache: &mut IdentifierMap<Warning>,
239    success_cache: &mut RuntimeIdentifierCache<Vec<ModuleIdentifier>>,
240    avoid_mutate_on_failure: bool,
241    statistics: &mut Statistics,
242    imports_cache: &mut RuntimeIdentifierCache<IdentifierIndexSet>,
243    module_cache: &HashMap<ModuleIdentifier, NoRuntimeModuleCache>,
244  ) -> Option<Warning> {
245    statistics
246      .module_visit
247      .entry(*module_id)
248      .and_modify(|count| {
249        *count += 1;
250      })
251      .or_insert(1);
252
253    if let Some(cache_entry) = failure_cache.get(module_id) {
254      statistics.cached += 1;
255      return Some(cache_entry.clone());
256    }
257
258    if config.has(module_id) {
259      statistics.already_in_config += 1;
260      return None;
261    }
262
263    let Compilation {
264      chunk_graph,
265      chunk_by_ukey,
266      ..
267    } = compilation;
268    let module_graph = compilation.get_module_graph();
269    let module_graph_cache = &compilation.module_graph_cache_artifact;
270
271    let incoming_modules = if let Some(incomings) = success_cache.get(module_id, runtime) {
272      statistics.cache_hit += 1;
273      incomings.clone()
274    } else {
275      let module = module_graph
276        .module_by_identifier(module_id)
277        .expect("should have module");
278      let module_readable_identifier = get_cached_readable_identifier(
279        module,
280        &compilation.module_static_cache_artifact,
281        &compilation.options.context,
282      );
283
284      if !possible_modules.contains(module_id) {
285        statistics.invalid_module += 1;
286        let problem = Warning::Id(*module_id);
287        failure_cache.insert(*module_id, problem.clone());
288        return Some(problem);
289      }
290
291      let missing_chunks: Vec<_> = chunk_graph
292        .get_module_chunks(config.root_module)
293        .iter()
294        .filter(|chunk| !chunk_graph.is_module_in_chunk(module_id, **chunk))
295        .collect();
296
297      if !missing_chunks.is_empty() {
298        let problem_string = {
299          let mut missing_chunks_list = missing_chunks
300            .iter()
301            .map(|&chunk| {
302              let chunk = chunk_by_ukey.expect_get(chunk);
303              chunk.name().unwrap_or("unnamed chunk(s)")
304            })
305            .collect::<Vec<_>>();
306          missing_chunks_list.sort_unstable();
307
308          let mut chunks = chunk_graph
309            .get_module_chunks(*module_id)
310            .iter()
311            .map(|&chunk| {
312              let chunk = chunk_by_ukey.expect_get(&chunk);
313              chunk.name().unwrap_or("unnamed chunk(s)")
314            })
315            .collect::<Vec<_>>();
316          chunks.sort_unstable();
317
318          format!(
319            "Module {} is not in the same chunk(s) (expected in chunk(s) {}, module is in chunk(s) {})",
320            module_readable_identifier,
321            missing_chunks_list.join(", "),
322            chunks.join(", ")
323          )
324        };
325
326        statistics.incorrect_chunks += 1;
327        let problem = Warning::Problem(problem_string);
328        failure_cache.insert(*module_id, problem.clone());
329        return Some(problem);
330      }
331
332      let NoRuntimeModuleCache { incomings, .. } = module_cache
333        .get(module_id)
334        .expect("should have module cache");
335
336      if let Some(incoming_connections_from_non_modules) = incomings.get(&None) {
337        let has_active_non_modules_connections = incoming_connections_from_non_modules
338          .iter()
339          .any(|connection| connection.is_active(&module_graph, runtime, module_graph_cache));
340
341        // TODO: ADD module connection explanations
342        if has_active_non_modules_connections {
343          let problem = {
344            // let importing_explanations = active_non_modules_connections
345            //   .iter()
346            //   .flat_map(|&c| c.explanation())
347            //   .collect::<HashSet<_>>();
348            // let mut explanations: Vec<_> = importing_explanations.into_iter().collect();
349            // explanations.sort();
350            format!(
351              "Module {module_readable_identifier} is referenced",
352              // if !explanations.is_empty() {
353              //   format!("by: {}", explanations.join(", "))
354              // } else {
355              //   "in an unsupported way".to_string()
356              // }
357            )
358          };
359          let problem = Warning::Problem(problem);
360          statistics.incorrect_dependency += 1;
361          failure_cache.insert(*module_id, problem.clone());
362          return Some(problem);
363        }
364      }
365
366      let mut incoming_connections_from_modules = HashMap::default();
367      for (origin_module, connections) in incomings.iter() {
368        if let Some(origin_module) = origin_module {
369          if chunk_graph.get_number_of_module_chunks(*origin_module) == 0 {
370            // Ignore connection from orphan modules
371            continue;
372          }
373
374          let mut origin_runtime = RuntimeSpec::default();
375          for r in chunk_graph.get_module_runtimes_iter(*origin_module, chunk_by_ukey) {
376            origin_runtime.extend(r);
377          }
378
379          let is_intersect = if let Some(runtime) = runtime {
380            runtime.intersection(&origin_runtime).count() > 0
381          } else {
382            false
383          };
384          if !is_intersect {
385            continue;
386          }
387
388          let active_connections: Vec<_> = connections
389            .iter()
390            .filter(|&connection| connection.is_active(&module_graph, runtime, module_graph_cache))
391            .collect();
392
393          if !active_connections.is_empty() {
394            incoming_connections_from_modules.insert(origin_module, active_connections);
395          }
396        }
397      }
398
399      let mut incoming_modules = incoming_connections_from_modules
400        .keys()
401        .map(|mid| **mid)
402        .collect::<Vec<_>>();
403      let other_chunk_modules = incoming_modules
404        .iter()
405        .filter(|&origin_module| {
406          chunk_graph
407            .get_module_chunks(config.root_module)
408            .iter()
409            .any(|&chunk_ukey| !chunk_graph.is_module_in_chunk(origin_module, chunk_ukey))
410        })
411        .collect::<Vec<_>>();
412
413      if !other_chunk_modules.is_empty() {
414        let problem = {
415          let mut names: Vec<_> = other_chunk_modules
416            .into_iter()
417            .map(|mid| {
418              let m = module_graph
419                .module_by_identifier(mid)
420                .expect("should have module");
421              get_cached_readable_identifier(
422                m,
423                &compilation.module_static_cache_artifact,
424                &compilation.options.context,
425              )
426            })
427            .collect();
428          names.sort();
429          format!(
430            "Module {} is referenced from different chunks by these modules: {}",
431            module_readable_identifier,
432            names.join(", ")
433          )
434        };
435
436        statistics.incorrect_chunks_of_importer += 1;
437        let problem = Warning::Problem(problem);
438        failure_cache.insert(*module_id, problem.clone());
439        return Some(problem);
440      }
441
442      let mut non_esm_connections = HashMap::default();
443      for (origin_module, connections) in incoming_connections_from_modules.iter() {
444        let has_non_esm_connections = connections.iter().any(|connection| {
445          if let Some(dep) = module_graph.dependency_by_id(&connection.dependency_id) {
446            !is_esm_dep_like(dep)
447          } else {
448            false
449          }
450        });
451
452        if has_non_esm_connections {
453          non_esm_connections.insert(origin_module, connections);
454        }
455      }
456
457      if !non_esm_connections.is_empty() {
458        let problem = {
459          let names: Vec<_> = non_esm_connections
460            .iter()
461            .map(|(origin_module, connections)| {
462              let module = module_graph
463                .module_by_identifier(origin_module)
464                .expect("should have module");
465              let readable_identifier = get_cached_readable_identifier(
466                module,
467                &compilation.module_static_cache_artifact,
468                &compilation.options.context,
469              );
470              let mut names = connections
471                .iter()
472                .filter_map(|item| {
473                  let dep = module_graph.dependency_by_id(&item.dependency_id)?;
474                  Some(dep.dependency_type().to_string())
475                })
476                .collect::<Vec<_>>();
477              names.sort();
478              format!(
479                "{} (referenced with {})",
480                readable_identifier,
481                names.join(",")
482              )
483            })
484            .collect();
485
486          format!(
487            "Module {} is referenced from these modules with unsupported syntax: {}",
488            module_readable_identifier,
489            names.join(", ")
490          )
491        };
492        let problem = Warning::Problem(problem);
493        statistics.incorrect_module_dependency += 1;
494        failure_cache.insert(*module_id, problem.clone());
495        return Some(problem);
496      }
497
498      if let Some(runtime) = runtime
499        && runtime.len() > 1
500      {
501        let mut other_runtime_connections = Vec::new();
502        'outer: for (origin_module, connections) in incoming_connections_from_modules {
503          let mut current_runtime_condition = RuntimeCondition::Boolean(false);
504          for connection in connections {
505            let runtime_condition = filter_runtime(Some(runtime), |runtime| {
506              connection.is_target_active(&module_graph, runtime, module_graph_cache)
507            });
508
509            if runtime_condition == RuntimeCondition::Boolean(false) {
510              continue;
511            }
512
513            if runtime_condition == RuntimeCondition::Boolean(true) {
514              continue 'outer;
515            }
516
517            // here two runtime_condition must be `RuntimeCondition::Spec`
518            if current_runtime_condition != RuntimeCondition::Boolean(false) {
519              current_runtime_condition
520                .as_spec_mut()
521                .expect("should be spec")
522                .extend(runtime_condition.as_spec().expect("should be spec"));
523            } else {
524              current_runtime_condition = runtime_condition;
525            }
526          }
527
528          if current_runtime_condition != RuntimeCondition::Boolean(false) {
529            other_runtime_connections.push((origin_module, current_runtime_condition));
530          }
531        }
532
533        if !other_runtime_connections.is_empty() {
534          let problem = {
535            format!(
536              "Module {} is runtime-dependent referenced by these modules: {}",
537              module_readable_identifier,
538              other_runtime_connections
539                .iter()
540                .map(|(origin_module, runtime_condition)| {
541                  let module = module_graph
542                    .module_by_identifier(origin_module)
543                    .expect("should have module");
544                  let readable_identifier = get_cached_readable_identifier(
545                    module,
546                    &compilation.module_static_cache_artifact,
547                    &compilation.options.context,
548                  );
549                  format!(
550                    "{} (expected runtime {}, module is only referenced in {})",
551                    readable_identifier,
552                    runtime,
553                    runtime_condition.as_spec().expect("should be spec")
554                  )
555                })
556                .collect::<Vec<_>>()
557                .join(", ")
558            )
559          };
560
561          let problem = Warning::Problem(problem);
562          statistics.incorrect_runtime_condition += 1;
563          failure_cache.insert(*module_id, problem.clone());
564          return Some(problem);
565        }
566      }
567
568      incoming_modules.sort();
569      success_cache.insert(*module_id, runtime, incoming_modules.clone());
570      incoming_modules
571    };
572
573    let backup = if avoid_mutate_on_failure {
574      Some(config.snapshot())
575    } else {
576      None
577    };
578
579    config.add(*module_id);
580
581    for origin_module in &incoming_modules {
582      if let Some(problem) = Self::try_to_add(
583        compilation,
584        config,
585        origin_module,
586        runtime,
587        active_runtime,
588        possible_modules,
589        candidates,
590        failure_cache,
591        success_cache,
592        false,
593        statistics,
594        imports_cache,
595        module_cache,
596      ) {
597        if let Some(backup) = &backup {
598          config.rollback(*backup);
599        }
600        statistics.importer_failed += 1;
601        failure_cache.insert(*module_id, problem.clone());
602        return Some(problem);
603      }
604    }
605
606    for imp in Self::get_imports(
607      &module_graph,
608      module_graph_cache,
609      *module_id,
610      runtime,
611      imports_cache,
612      module_cache,
613    ) {
614      candidates.insert(imp);
615    }
616    statistics.added += 1;
617    None
618  }
619
620  pub async fn process_concatenated_configuration(
621    compilation: &mut Compilation,
622    config: ConcatConfiguration,
623    used_modules: &mut HashSet<ModuleIdentifier>,
624  ) -> Result<()> {
625    let module_graph = compilation.get_module_graph();
626
627    let root_module_id = config.root_module;
628    if used_modules.contains(&root_module_id) {
629      return Ok(());
630    }
631
632    let modules_set = config.get_modules();
633    for m in modules_set {
634      used_modules.insert(*m);
635    }
636    let box_module = module_graph
637      .module_by_identifier(&root_module_id)
638      .expect("should have module");
639    let root_module_source_types = box_module.source_types(&module_graph);
640    let is_root_module_asset_module = root_module_source_types.contains(&SourceType::Asset);
641
642    let root_module_ctxt = RootModuleContext {
643      id: root_module_id,
644      readable_identifier: get_cached_readable_identifier(
645        box_module,
646        &compilation.module_static_cache_artifact,
647        &compilation.options.context,
648      ),
649      name_for_condition: box_module.name_for_condition().clone(),
650      lib_indent: box_module
651        .lib_ident(LibIdentOptions {
652          context: compilation.options.context.as_str(),
653        })
654        .map(|id| id.to_string()),
655      layer: box_module.get_layer().cloned(),
656      resolve_options: box_module.get_resolve_options().clone(),
657      code_generation_dependencies: box_module
658        .get_code_generation_dependencies()
659        .map(|deps| deps.to_vec()),
660      presentational_dependencies: box_module
661        .get_presentational_dependencies()
662        .map(|deps| deps.to_vec()),
663      context: Some(compilation.options.context.clone()),
664      side_effect_connection_state: box_module.get_side_effects_connection_state(
665        &module_graph,
666        &compilation.module_graph_cache_artifact,
667        &mut IdentifierSet::default(),
668        &mut IdentifierMap::default(),
669      ),
670      factory_meta: box_module.factory_meta().cloned(),
671      build_meta: box_module.build_meta().clone(),
672      module_argument: box_module.get_module_argument(),
673      exports_argument: box_module.get_exports_argument(),
674    };
675    let modules = modules_set
676      .iter()
677      .map(|id| {
678        let module = module_graph
679          .module_by_identifier(id)
680          .unwrap_or_else(|| panic!("should have module {id}"));
681
682        ConcatenatedInnerModule {
683          id: *id,
684          size: module.size(
685            Some(&rspack_core::SourceType::JavaScript),
686            Some(compilation),
687          ),
688          original_source_hash: module.source().map(|source| {
689            let mut hasher = DefaultHasher::default();
690            source.dyn_hash(&mut hasher);
691            hasher.finish()
692          }),
693          shorten_id: get_cached_readable_identifier(
694            module,
695            &compilation.module_static_cache_artifact,
696            &compilation.options.context,
697          ),
698        }
699      })
700      .collect::<Vec<_>>();
701    let mut new_module = ConcatenatedModule::create(
702      root_module_ctxt,
703      modules,
704      Some(rspack_hash::HashFunction::MD4),
705      config.runtime.clone(),
706      compilation,
707    );
708    new_module
709      .build(
710        rspack_core::BuildContext {
711          compiler_id: compilation.compiler_id(),
712          compilation_id: compilation.id(),
713          resolver_factory: compilation.resolver_factory.clone(),
714          plugin_driver: compilation.plugin_driver.clone(),
715          compiler_options: compilation.options.clone(),
716          fs: compilation.input_filesystem.clone(),
717        },
718        Some(compilation),
719      )
720      .await?;
721    let mut chunk_graph = std::mem::take(&mut compilation.chunk_graph);
722    let mut module_graph = compilation.get_module_graph_mut();
723    let root_mgm_exports = module_graph
724      .module_graph_module_by_identifier(&root_module_id)
725      .expect("should have mgm")
726      .exports;
727    let module_graph_module = ModuleGraphModule::new(new_module.id(), root_mgm_exports);
728    module_graph.add_module_graph_module(module_graph_module);
729    ModuleGraph::clone_module_attributes(compilation, &root_module_id, &new_module.id());
730    // integrate
731
732    let mut module_graph = compilation.get_module_graph_mut();
733    for m in modules_set {
734      if m == &root_module_id {
735        continue;
736      }
737      module_graph.copy_outgoing_module_connections(m, &new_module.id(), |con, dep| {
738        con.original_module_identifier.as_ref() == Some(m)
739          && !(is_esm_dep_like(dep) && modules_set.contains(con.module_identifier()))
740      });
741      // TODO: optimize asset module https://github.com/webpack/webpack/pull/15515/files
742      for chunk_ukey in chunk_graph.get_module_chunks(root_module_id).clone() {
743        let module = module_graph
744          .module_by_identifier(m)
745          .expect("should exist module");
746
747        let source_types =
748          chunk_graph.get_chunk_module_source_types(&chunk_ukey, module, &module_graph);
749
750        if source_types.len() == 1 {
751          chunk_graph.disconnect_chunk_and_module(&chunk_ukey, *m);
752        } else {
753          let new_source_types = source_types
754            .into_iter()
755            .filter(|source_type| !matches!(source_type, SourceType::JavaScript))
756            .collect();
757          chunk_graph.set_chunk_modules_source_types(&chunk_ukey, *m, new_source_types)
758        }
759      }
760    }
761
762    // different from webpack
763    // Rspack: if entry is an asset module, outputs a js chunk and a asset chunk
764    // Webpack: if entry is an asset module, outputs an asset chunk
765    // these lines of codes fix a bug: when asset module (NormalModule) is concatenated into ConcatenatedModule, the asset will be lost
766    // because `chunk_graph.replace_module(&root_module_id, &new_module.id());` will remove the asset module from chunk, and I add this module back to fix this bug
767    if is_root_module_asset_module {
768      chunk_graph.replace_module(&root_module_id, &new_module.id());
769      chunk_graph.add_module(root_module_id);
770      for chunk_ukey in chunk_graph.get_module_chunks(new_module.id()).clone() {
771        let module = module_graph
772          .module_by_identifier(&root_module_id)
773          .expect("should exist module");
774
775        let source_types =
776          chunk_graph.get_chunk_module_source_types(&chunk_ukey, module, &module_graph);
777        let new_source_types = source_types
778          .iter()
779          .filter(|source_type| !matches!(source_type, SourceType::JavaScript))
780          .copied()
781          .collect();
782        chunk_graph.set_chunk_modules_source_types(&chunk_ukey, root_module_id, new_source_types);
783        chunk_graph.connect_chunk_and_module(chunk_ukey, root_module_id);
784      }
785    } else {
786      chunk_graph.replace_module(&root_module_id, &new_module.id());
787    }
788
789    module_graph.move_module_connections(&root_module_id, &new_module.id(), |c, dep| {
790      let other_module = if *c.module_identifier() == root_module_id {
791        c.original_module_identifier
792      } else {
793        Some(*c.module_identifier())
794      };
795      let inner_connection = is_esm_dep_like(dep)
796        && if let Some(other_module) = other_module {
797          modules_set.contains(&other_module)
798        } else {
799          false
800        };
801      !inner_connection
802    });
803    module_graph.add_module(new_module.boxed());
804    compilation.chunk_graph = chunk_graph;
805    Ok(())
806  }
807
808  async fn optimize_chunk_modules_impl(&self, compilation: &mut Compilation) -> Result<()> {
809    let logger = compilation.get_logger("rspack.ModuleConcatenationPlugin");
810    let mut relevant_modules = vec![];
811    let mut possible_inners = IdentifierSet::default();
812    let start = logger.time("select relevant modules");
813    let module_graph = compilation.get_module_graph();
814
815    // filter modules that can be root
816    let modules: Vec<_> = module_graph
817      .module_graph_modules()
818      .keys()
819      .copied()
820      .collect();
821    let res: Vec<_> = modules
822      .into_par_iter()
823      .map(|module_id| {
824        let mut can_be_root = true;
825        let mut can_be_inner = true;
826        let mut bailout_reason = vec![];
827        let number_of_module_chunks = compilation
828          .chunk_graph
829          .get_number_of_module_chunks(module_id);
830        let is_entry_module = compilation.chunk_graph.is_entry_module(&module_id);
831        let module_graph = compilation.get_module_graph();
832        let m = module_graph.module_by_identifier(&module_id);
833
834        if let Some(reason) = m
835          .expect("should have module")
836          .get_concatenation_bailout_reason(&module_graph, &compilation.chunk_graph)
837        {
838          bailout_reason.push(reason);
839          return (false, false, module_id, bailout_reason);
840        }
841
842        let m = module_graph.module_by_identifier(&module_id);
843
844        if ModuleGraph::is_async(compilation, &module_id) {
845          bailout_reason.push("Module is async".into());
846          return (false, false, module_id, bailout_reason);
847        }
848
849        if !m.expect("should have module").build_info().strict {
850          bailout_reason.push("Module is not in strict mode".into());
851          return (false, false, module_id, bailout_reason);
852        }
853        if number_of_module_chunks == 0 {
854          bailout_reason.push("Module is not in any chunk".into());
855          return (false, false, module_id, bailout_reason);
856        }
857
858        let exports_info =
859          module_graph.get_prefetched_exports_info(&module_id, PrefetchExportsInfoMode::Default);
860        let relevant_exports = exports_info.get_relevant_exports(None);
861        let unknown_exports = relevant_exports
862          .iter()
863          .filter(|export_info| {
864            export_info.is_reexport() && get_target(export_info, &module_graph).is_none()
865          })
866          .copied()
867          .collect::<Vec<_>>();
868        if !unknown_exports.is_empty() {
869          let cur_bailout_reason = unknown_exports
870            .into_iter()
871            .map(|export_info| {
872              let name = export_info
873                .name()
874                .map(|name| name.to_string())
875                .unwrap_or("other exports".to_string());
876              format!("{} : {}", name, export_info.get_used_info())
877            })
878            .collect::<Vec<String>>()
879            .join(", ");
880          // self.set_bailout_reason(
881          //   &module_id,
882          //   format!("Reexports in this module do not have a static target ({bailout_reason})"),
883          //   &mut module_graph,
884          // );
885
886          bailout_reason.push(
887            format!("Reexports in this module do not have a static target ({cur_bailout_reason})")
888              .into(),
889          );
890
891          return (false, false, module_id, bailout_reason);
892        }
893        let unknown_provided_exports = relevant_exports
894          .iter()
895          .filter(|export_info| !matches!(export_info.provided(), Some(ExportProvided::Provided)))
896          .copied()
897          .collect::<Vec<_>>();
898
899        if !unknown_provided_exports.is_empty() {
900          let cur_bailout_reason = unknown_provided_exports
901            .into_iter()
902            .map(|export_info| {
903              let name = export_info
904                .name()
905                .map(|name| name.to_string())
906                .unwrap_or("other exports".to_string());
907              format!(
908                "{} : {} and {}",
909                name,
910                export_info.get_provided_info(),
911                export_info.get_used_info(),
912              )
913            })
914            .collect::<Vec<String>>()
915            .join(", ");
916          // self.set_bailout_reason(
917          //   &module_id,
918          //   format!("List of module exports is dynamic ({bailout_reason})"),
919          //   &mut module_graph,
920          // );
921          bailout_reason
922            .push(format!("List of module exports is dynamic ({cur_bailout_reason})").into());
923          can_be_root = false;
924        }
925
926        if is_entry_module {
927          // self.set_bailout_reason(
928          //   &module_id,
929          //   "Module is an entry point".to_string(),
930          //   &mut module_graph,
931          // );
932          can_be_inner = false;
933          bailout_reason.push("Module is an entry point".into());
934        }
935        (can_be_root, can_be_inner, module_id, bailout_reason)
936        // if can_be_root {
937        //   relevant_modules.push(module_id);
938        // }
939        // if can_be_inner {
940        //   possible_inners.insert(module_id);
941        // }
942      })
943      .collect();
944
945    let mut module_graph = compilation.get_module_graph_mut();
946
947    for (can_be_root, can_be_inner, module_id, bailout_reason) in res {
948      if can_be_root {
949        relevant_modules.push(module_id);
950      }
951      if can_be_inner {
952        possible_inners.insert(module_id);
953      }
954      for bailout_reason in bailout_reason {
955        self.set_bailout_reason(&module_id, bailout_reason, &mut module_graph);
956      }
957    }
958
959    let module_graph = compilation.get_module_graph();
960    logger.time_end(start);
961    let mut relevant_len_buffer = itoa::Buffer::new();
962    let relevant_len_str = relevant_len_buffer.format(relevant_modules.len());
963    let mut possible_len_buffer = itoa::Buffer::new();
964    let possible_len_str = possible_len_buffer.format(possible_inners.len());
965    logger.debug(format!(
966      "{} potential root modules, {} potential inner modules",
967      relevant_len_str, possible_len_str,
968    ));
969
970    let start = logger.time("sort relevant modules");
971    relevant_modules.sort_by(|a, b| {
972      let ad = module_graph.get_depth(a);
973      let bd = module_graph.get_depth(b);
974      ad.cmp(&bd)
975    });
976
977    logger.time_end(start);
978    let mut statistics = Statistics::default();
979    let mut stats_candidates = 0;
980    let mut stats_size_sum = 0;
981    let mut stats_empty_configurations = 0;
982
983    let start = logger.time("find modules to concatenate");
984    let mut concat_configurations: Vec<ConcatConfiguration> = Vec::new();
985    let mut used_as_inner: IdentifierSet = IdentifierSet::default();
986    let mut imports_cache = RuntimeIdentifierCache::<IdentifierIndexSet>::default();
987
988    let module_graph = compilation.get_module_graph();
989    let module_graph_cache = &compilation.module_graph_cache_artifact;
990    let module_static_cache_artifact = &compilation.module_static_cache_artifact;
991    let compilation_context = &compilation.options.context;
992    let modules_without_runtime_cache = relevant_modules
993      .iter()
994      .chain(possible_inners.iter())
995      .par_bridge()
996      .map(|module_id| {
997        let exports_info = module_graph.get_exports_info(module_id);
998        let exports_info_data = ExportsInfoGetter::prefetch(
999          &exports_info,
1000          &module_graph,
1001          PrefetchExportsInfoMode::Default,
1002        );
1003        let provided_names = matches!(
1004          exports_info_data.get_provided_exports(),
1005          ProvidedExports::ProvidedNames(_)
1006        );
1007        let module = module_graph
1008          .module_by_identifier(module_id)
1009          .expect("should have module");
1010        let mut runtime = RuntimeSpec::default();
1011        for r in compilation
1012          .chunk_graph
1013          .get_module_runtimes_iter(*module_id, &compilation.chunk_by_ukey)
1014        {
1015          runtime.extend(r);
1016        }
1017
1018        let _ =
1019          get_cached_readable_identifier(module, module_static_cache_artifact, compilation_context);
1020
1021        let connections = module
1022          .get_dependencies()
1023          .iter()
1024          .filter_map(|d| {
1025            let dep = module_graph.dependency_by_id(d)?;
1026            if !is_esm_dep_like(dep) {
1027              return None;
1028            }
1029            let con = module_graph.connection_by_dependency_id(d)?;
1030            let module_dep = dep.as_module_dependency().expect("should be module dep");
1031            let imported_names =
1032              module_dep.get_referenced_exports(&module_graph, module_graph_cache, None);
1033
1034            Some((
1035              con.clone(),
1036              (
1037                imported_names.iter().all(|item| match item {
1038                  ExtendedReferencedExport::Array(arr) => !arr.is_empty(),
1039                  ExtendedReferencedExport::Export(export) => !export.name.is_empty(),
1040                }),
1041                con.is_target_active(&module_graph, Some(&runtime), module_graph_cache),
1042              ),
1043            ))
1044          })
1045          .collect::<Vec<_>>();
1046
1047        let incomings = module_graph.get_incoming_connections_by_origin_module(module_id);
1048        (
1049          *module_id,
1050          NoRuntimeModuleCache {
1051            runtime,
1052            provided_names,
1053            connections,
1054            incomings,
1055          },
1056        )
1057      })
1058      .collect::<HashMap<_, _>>();
1059
1060    for current_root in relevant_modules.iter() {
1061      if used_as_inner.contains(current_root) {
1062        continue;
1063      }
1064
1065      let NoRuntimeModuleCache { runtime, .. } = modules_without_runtime_cache
1066        .get(current_root)
1067        .expect("should have module");
1068      let module_graph = compilation.get_module_graph();
1069      let module_graph_cache = &compilation.module_graph_cache_artifact;
1070      let exports_info = module_graph.get_exports_info(current_root);
1071      let exports_info_data = ExportsInfoGetter::prefetch(
1072        &exports_info,
1073        &module_graph,
1074        PrefetchExportsInfoMode::Default,
1075      );
1076      let filtered_runtime = filter_runtime(Some(runtime), |r| exports_info_data.is_module_used(r));
1077      let active_runtime = match filtered_runtime {
1078        RuntimeCondition::Boolean(true) => Some(runtime.clone()),
1079        RuntimeCondition::Boolean(false) => None,
1080        RuntimeCondition::Spec(spec) => Some(spec),
1081      };
1082
1083      let mut current_configuration =
1084        ConcatConfiguration::new(*current_root, active_runtime.clone());
1085
1086      let mut failure_cache = IdentifierMap::default();
1087      let mut success_cache = RuntimeIdentifierCache::default();
1088      let mut candidates_visited = HashSet::default();
1089      let mut candidates = VecDeque::new();
1090
1091      let imports = Self::get_imports(
1092        &module_graph,
1093        module_graph_cache,
1094        *current_root,
1095        active_runtime.as_ref(),
1096        &mut imports_cache,
1097        &modules_without_runtime_cache,
1098      );
1099      for import in imports {
1100        candidates.push_back(import);
1101      }
1102
1103      let mut import_candidates = IdentifierSet::default();
1104      while let Some(imp) = candidates.pop_front() {
1105        if candidates_visited.contains(&imp) {
1106          continue;
1107        } else {
1108          candidates_visited.insert(imp);
1109        }
1110        import_candidates.clear();
1111        match Self::try_to_add(
1112          compilation,
1113          &mut current_configuration,
1114          &imp,
1115          Some(runtime),
1116          active_runtime.as_ref(),
1117          &possible_inners,
1118          &mut import_candidates,
1119          &mut failure_cache,
1120          &mut success_cache,
1121          true,
1122          &mut statistics,
1123          &mut imports_cache,
1124          &modules_without_runtime_cache,
1125        ) {
1126          Some(problem) => {
1127            failure_cache.insert(imp, problem.clone());
1128            current_configuration.add_warning(imp, problem);
1129          }
1130          _ => {
1131            import_candidates.iter().for_each(|c: &ModuleIdentifier| {
1132              candidates.push_back(*c);
1133            });
1134          }
1135        }
1136      }
1137      stats_candidates += candidates.len();
1138      if !current_configuration.is_empty() {
1139        let modules = current_configuration.get_modules();
1140        stats_size_sum += modules.len();
1141        let root_module = current_configuration.root_module;
1142
1143        modules.iter().for_each(|module| {
1144          if *module != root_module {
1145            used_as_inner.insert(*module);
1146          }
1147        });
1148        concat_configurations.push(current_configuration);
1149      } else {
1150        stats_empty_configurations += 1;
1151        let mut module_graph = compilation.get_module_graph_mut();
1152        let optimization_bailouts = module_graph.get_optimization_bailout_mut(current_root);
1153        for warning in current_configuration.get_warnings_sorted() {
1154          optimization_bailouts.push(self.format_bailout_warning(warning.0, &warning.1));
1155        }
1156      }
1157    }
1158
1159    logger.time_end(start);
1160    if !concat_configurations.is_empty() {
1161      let mut concat_len_buffer = itoa::Buffer::new();
1162      let concat_len_str = concat_len_buffer.format(concat_configurations.len());
1163      let mut avg_size_buffer = itoa::Buffer::new();
1164      let avg_size_str = avg_size_buffer.format(stats_size_sum / concat_configurations.len());
1165      let mut empty_configs_buffer = itoa::Buffer::new();
1166      let empty_configs_str = empty_configs_buffer.format(stats_empty_configurations);
1167      logger.debug(format!(
1168        "{} successful concat configurations (avg size: {}), {} bailed out completely",
1169        concat_len_str, avg_size_str, empty_configs_str
1170      ));
1171    }
1172
1173    let mut candidates_buffer = itoa::Buffer::new();
1174    let candidates_str = candidates_buffer.format(stats_candidates);
1175    let mut cached_buffer = itoa::Buffer::new();
1176    let cached_str = cached_buffer.format(statistics.cached);
1177    let mut already_in_config_buffer = itoa::Buffer::new();
1178    let already_in_config_str = already_in_config_buffer.format(statistics.already_in_config);
1179    let mut invalid_module_buffer = itoa::Buffer::new();
1180    let invalid_module_str = invalid_module_buffer.format(statistics.invalid_module);
1181    let mut incorrect_chunks_buffer = itoa::Buffer::new();
1182    let incorrect_chunks_str = incorrect_chunks_buffer.format(statistics.incorrect_chunks);
1183    let mut incorrect_dependency_buffer = itoa::Buffer::new();
1184    let incorrect_dependency_str =
1185      incorrect_dependency_buffer.format(statistics.incorrect_dependency);
1186    let mut incorrect_chunks_of_importer_buffer = itoa::Buffer::new();
1187    let incorrect_chunks_of_importer_str =
1188      incorrect_chunks_of_importer_buffer.format(statistics.incorrect_chunks_of_importer);
1189    let mut incorrect_module_dependency_buffer = itoa::Buffer::new();
1190    let incorrect_module_dependency_str =
1191      incorrect_module_dependency_buffer.format(statistics.incorrect_module_dependency);
1192    let mut incorrect_runtime_condition_buffer = itoa::Buffer::new();
1193    let incorrect_runtime_condition_str =
1194      incorrect_runtime_condition_buffer.format(statistics.incorrect_runtime_condition);
1195    let mut importer_failed_buffer = itoa::Buffer::new();
1196    let importer_failed_str = importer_failed_buffer.format(statistics.importer_failed);
1197    let mut added_buffer = itoa::Buffer::new();
1198    let added_str = added_buffer.format(statistics.added);
1199    logger.debug(format!(
1200        "{} candidates were considered for adding ({} cached failure, {} already in config, {} invalid module, {} incorrect chunks, {} incorrect dependency, {} incorrect chunks of importer, {} incorrect module dependency, {} incorrect runtime condition, {} importer failed, {} added)",
1201        candidates_str,
1202        cached_str,
1203        already_in_config_str,
1204        invalid_module_str,
1205        incorrect_chunks_str,
1206        incorrect_dependency_str,
1207        incorrect_chunks_of_importer_str,
1208        incorrect_module_dependency_str,
1209        incorrect_runtime_condition_str,
1210        importer_failed_str,
1211        added_str
1212    ));
1213
1214    // Copy from  https://github.com/webpack/webpack/blob/1f99ad6367f2b8a6ef17cce0e058f7a67fb7db18/lib/optimize/ModuleConcatenationPlugin.js#L368-L371
1215    // HACK: Sort configurations by length and start with the longest one
1216    // to get the biggest groups possible. Used modules are marked with usedModules
1217    // TODO(from webpack): Allow reusing existing configuration while trying to add dependencies.
1218    // This would improve performance. O(n^2) -> O(n)
1219    let start = logger.time("sort concat configurations");
1220    concat_configurations.sort_by(|a, b| b.modules.len().cmp(&a.modules.len()));
1221    logger.time_end(start);
1222
1223    let mut used_modules = HashSet::default();
1224
1225    for config in concat_configurations {
1226      Self::process_concatenated_configuration(compilation, config, &mut used_modules).await?;
1227    }
1228    Ok(())
1229  }
1230}
1231
1232#[plugin_hook(CompilationOptimizeChunkModules for ModuleConcatenationPlugin)]
1233async fn optimize_chunk_modules(&self, compilation: &mut Compilation) -> Result<Option<bool>> {
1234  if let Some(diagnostic) = compilation.incremental.disable_passes(
1235    IncrementalPasses::MODULES_HASHES
1236    | IncrementalPasses::MODULE_IDS
1237    | IncrementalPasses::CHUNK_IDS
1238    | IncrementalPasses::CHUNKS_RUNTIME_REQUIREMENTS
1239    | IncrementalPasses::CHUNKS_HASHES,
1240    "ModuleConcatenationPlugin (optimization.concatenateModules = true)",
1241    "it requires calculating the modules that can be concatenated based on all the modules, which is a global effect",
1242  ) {
1243    if let Some(diagnostic) = diagnostic {
1244      compilation.push_diagnostic(diagnostic);
1245    }
1246    compilation.cgm_hash_artifact.clear();
1247    compilation.module_ids_artifact.clear();
1248    compilation.chunk_ids_artifact.clear();
1249    compilation.cgc_runtime_requirements_artifact.clear();
1250    compilation.chunk_hashes_artifact.clear();
1251  }
1252
1253  self.optimize_chunk_modules_impl(compilation).await?;
1254  Ok(None)
1255}
1256
1257impl Plugin for ModuleConcatenationPlugin {
1258  fn apply(&self, ctx: PluginContext<&mut ApplyContext>, _options: &CompilerOptions) -> Result<()> {
1259    ctx
1260      .context
1261      .compilation_hooks
1262      .optimize_chunk_modules
1263      .tap(optimize_chunk_modules::new(self));
1264    Ok(())
1265  }
1266}
1267
1268#[derive(Debug, Default)]
1269struct Statistics {
1270  cached: u32,
1271  already_in_config: u32,
1272  invalid_module: u32,
1273  incorrect_chunks: u32,
1274  incorrect_dependency: u32,
1275  incorrect_module_dependency: u32,
1276  incorrect_chunks_of_importer: u32,
1277  incorrect_runtime_condition: u32,
1278  importer_failed: u32,
1279  cache_hit: u32,
1280  module_visit: IdentifierMap<usize>,
1281  added: u32,
1282}
1283
1284#[derive(Debug)]
1285pub struct NoRuntimeModuleCache {
1286  runtime: RuntimeSpec,
1287  provided_names: bool,
1288  connections: Vec<(ModuleGraphConnection, (bool, bool))>,
1289  incomings: HashMap<Option<ModuleIdentifier>, Vec<ModuleGraphConnection>>,
1290}