Skip to main content

rspack_plugin_javascript/plugin/
module_concatenation_plugin.rs

1#![allow(clippy::only_used_in_recursion)]
2use std::{borrow::Cow, collections::VecDeque, rc::Rc, sync::Arc};
3
4use rayon::prelude::*;
5use rspack_collections::{IdentifierDashMap, IdentifierIndexSet, IdentifierMap, IdentifierSet};
6use rspack_core::{
7  BoxDependency, Compilation, CompilationOptimizeChunkModules, DependencyId, DependencyType,
8  ExportProvided, ExtendedReferencedExport, GetTargetResult, ImportedByDeferModulesArtifact,
9  LibIdentOptions, Logger, Module, ModuleExt, ModuleGraph, ModuleGraphCacheArtifact,
10  ModuleGraphConnection, ModuleGraphModule, ModuleIdentifier, Plugin, PrefetchExportsInfoMode,
11  ProvidedExports, RuntimeCondition, RuntimeSpec, SourceType,
12  concatenated_module::{
13    ConcatenatedInnerModule, ConcatenatedModule, RootModuleContext, is_esm_dep_like,
14  },
15  filter_runtime, get_cached_readable_identifier, get_target,
16  incremental::IncrementalPasses,
17};
18use rspack_error::{Result, ToStringResultToRspackResultExt};
19use rspack_hook::{plugin, plugin_hook};
20use rspack_util::itoa;
21use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
22
23fn format_bailout_reason(msg: &str) -> String {
24  format!("ModuleConcatenation bailout: {msg}")
25}
26
27#[derive(Clone, Debug)]
28enum Warning {
29  Id(ModuleIdentifier),
30  Problem(String),
31}
32
33#[derive(Debug, Clone)]
34pub struct ConcatConfiguration {
35  pub root_module: ModuleIdentifier,
36  runtime: Option<RuntimeSpec>,
37  modules: IdentifierIndexSet,
38  warnings: IdentifierMap<Warning>,
39}
40
41impl ConcatConfiguration {
42  pub fn new(root_module: ModuleIdentifier, runtime: Option<RuntimeSpec>) -> Self {
43    let mut modules = IdentifierIndexSet::default();
44    modules.insert(root_module);
45
46    ConcatConfiguration {
47      root_module,
48      runtime,
49      modules,
50      warnings: IdentifierMap::default(),
51    }
52  }
53
54  fn add(&mut self, module: ModuleIdentifier) {
55    self.modules.insert(module);
56  }
57
58  fn has(&self, module: &ModuleIdentifier) -> bool {
59    self.modules.contains(module)
60  }
61
62  fn is_empty(&self) -> bool {
63    self.modules.len() == 1
64  }
65
66  fn add_warning(&mut self, module: ModuleIdentifier, problem: Warning) {
67    self.warnings.insert(module, problem);
68  }
69
70  fn get_warnings_sorted(&self) -> Vec<(ModuleIdentifier, Warning)> {
71    let mut sorted_warnings: Vec<_> = self.warnings.clone().into_iter().collect();
72    sorted_warnings.sort_by_key(|(id, _)| *id);
73    sorted_warnings
74  }
75
76  fn get_modules(&self) -> &IdentifierIndexSet {
77    &self.modules
78  }
79
80  fn snapshot(&self) -> usize {
81    self.modules.len()
82  }
83
84  fn rollback(&mut self, snapshot: usize) {
85    let modules = &mut self.modules;
86    let len = modules.len();
87    for _ in snapshot..len {
88      modules.pop();
89    }
90  }
91}
92
93#[plugin]
94#[derive(Debug, Default)]
95pub struct ModuleConcatenationPlugin {
96  bailout_reason_map: IdentifierDashMap<Arc<Cow<'static, str>>>,
97}
98
99#[derive(Default)]
100pub struct RuntimeIdentifierCache<T> {
101  no_runtime_map: IdentifierMap<T>,
102  runtime_map: HashMap<RuntimeSpec, IdentifierMap<T>>,
103}
104
105impl<T> RuntimeIdentifierCache<T> {
106  fn insert(&mut self, module: ModuleIdentifier, runtime: Option<&RuntimeSpec>, value: T) {
107    if let Some(runtime) = runtime {
108      if let Some(map) = self.runtime_map.get_mut(runtime) {
109        map.insert(module, value);
110      } else {
111        let mut map = IdentifierMap::default();
112        map.insert(module, value);
113        self.runtime_map.insert(runtime.clone(), map);
114      }
115    } else {
116      self.no_runtime_map.insert(module, value);
117    }
118  }
119
120  fn get(&self, module: &ModuleIdentifier, runtime: Option<&RuntimeSpec>) -> Option<&T> {
121    if let Some(runtime) = runtime {
122      let map = self.runtime_map.get(runtime)?;
123
124      map.get(module)
125    } else {
126      self.no_runtime_map.get(module)
127    }
128  }
129}
130
131impl ModuleConcatenationPlugin {
132  fn format_bailout_warning(&self, module: ModuleIdentifier, warning: &Warning) -> String {
133    match warning {
134      Warning::Problem(id) => format_bailout_reason(&format!("Cannot concat with {module}: {id}")),
135      Warning::Id(id) => {
136        let reason = self.get_inner_bailout_reason(id);
137        let reason_with_prefix = match reason {
138          Some(reason) => format!(": {}", *reason),
139          None => "".to_string(),
140        };
141        if id == &module {
142          format_bailout_reason(&format!("Cannot concat with {module}{reason_with_prefix}"))
143        } else {
144          format_bailout_reason(&format!(
145            "Cannot concat with {module} because of {id}{reason_with_prefix}"
146          ))
147        }
148      }
149    }
150  }
151
152  fn set_bailout_reason(
153    &self,
154    module: &ModuleIdentifier,
155    reason: Cow<'static, str>,
156    mg: &mut ModuleGraph,
157  ) {
158    self.set_inner_bailout_reason(module, reason.clone());
159    mg.get_optimization_bailout_mut(module)
160      .push(format_bailout_reason(&reason));
161  }
162
163  fn set_inner_bailout_reason(&self, module: &ModuleIdentifier, reason: Cow<'static, str>) {
164    self.bailout_reason_map.insert(*module, Arc::new(reason));
165  }
166
167  fn get_inner_bailout_reason(
168    &self,
169    module_id: &ModuleIdentifier,
170  ) -> Option<Arc<Cow<'static, str>>> {
171    self
172      .bailout_reason_map
173      .get(module_id)
174      .map(|reason| reason.clone())
175  }
176
177  pub fn get_imports(
178    mg: &ModuleGraph,
179    mg_cache: &ModuleGraphCacheArtifact,
180    mi: ModuleIdentifier,
181    runtime: Option<&RuntimeSpec>,
182    imports_cache: &mut RuntimeIdentifierCache<IdentifierIndexSet>,
183    module_cache: &HashMap<ModuleIdentifier, NoRuntimeModuleCache>,
184  ) -> IdentifierIndexSet {
185    if let Some(set) = imports_cache.get(&mi, runtime) {
186      return set.clone();
187    }
188
189    let cached = module_cache.get(&mi).expect("should have module");
190
191    let mut set = IdentifierIndexSet::default();
192    for (con, (has_imported_names, cached_active)) in &cached.connections {
193      if set.contains(con.module_identifier()) {
194        continue;
195      }
196
197      let is_target_active = if let Some(runtime) = runtime {
198        if cached.runtime == *runtime {
199          // runtime is same, use cached value
200          *cached_active
201        } else if *cached_active && cached.runtime.is_subset(runtime) {
202          // cached runtime is subset and active, means it is also active in current runtime
203          true
204        } else if !*cached_active && cached.runtime.is_superset(runtime) {
205          // cached runtime is superset and inactive, means it is also inactive in current runtime
206          false
207        } else {
208          // can't determine, need to check
209          con.is_target_active(mg, Some(runtime), mg_cache)
210        }
211      } else {
212        // no runtime, need to check
213        con.is_target_active(mg, None, mg_cache)
214      };
215
216      if !is_target_active {
217        continue;
218      }
219      if *has_imported_names || cached.provided_names {
220        set.insert(*con.module_identifier());
221      }
222    }
223
224    imports_cache.insert(mi, runtime, set.clone());
225    set
226  }
227
228  #[allow(clippy::too_many_arguments)]
229  fn try_to_add(
230    compilation: &Compilation,
231    config: &mut ConcatConfiguration,
232    module_id: &ModuleIdentifier,
233    runtime: Option<&RuntimeSpec>,
234    active_runtime: Option<&RuntimeSpec>,
235    possible_modules: &IdentifierSet,
236    candidates: &mut IdentifierSet,
237    failure_cache: &mut IdentifierMap<Warning>,
238    success_cache: &mut RuntimeIdentifierCache<Vec<ModuleIdentifier>>,
239    avoid_mutate_on_failure: bool,
240    statistics: &mut Statistics,
241    imports_cache: &mut RuntimeIdentifierCache<IdentifierIndexSet>,
242    module_cache: &HashMap<ModuleIdentifier, NoRuntimeModuleCache>,
243  ) -> Option<Warning> {
244    statistics
245      .module_visit
246      .entry(*module_id)
247      .and_modify(|count| {
248        *count += 1;
249      })
250      .or_insert(1);
251
252    if let Some(cache_entry) = failure_cache.get(module_id) {
253      statistics.cached += 1;
254      return Some(cache_entry.clone());
255    }
256
257    if config.has(module_id) {
258      statistics.already_in_config += 1;
259      return None;
260    }
261
262    let Compilation {
263      chunk_graph,
264      chunk_by_ukey,
265      ..
266    } = compilation;
267    let module_graph = compilation.get_module_graph();
268    let module_graph_cache = &compilation.module_graph_cache_artifact;
269
270    let incoming_modules = if let Some(incomings) = success_cache.get(module_id, runtime) {
271      statistics.cache_hit += 1;
272      incomings.clone()
273    } else {
274      let module_readable_identifier = get_cached_readable_identifier(
275        module_id,
276        module_graph,
277        &compilation.module_static_cache_artifact,
278        &compilation.options.context,
279      );
280
281      if !possible_modules.contains(module_id) {
282        statistics.invalid_module += 1;
283        let problem = Warning::Id(*module_id);
284        failure_cache.insert(*module_id, problem.clone());
285        return Some(problem);
286      }
287
288      let missing_chunks: Vec<_> = chunk_graph
289        .get_module_chunks(config.root_module)
290        .iter()
291        .filter(|chunk| !chunk_graph.is_module_in_chunk(module_id, **chunk))
292        .collect();
293
294      if !missing_chunks.is_empty() {
295        let problem_string = {
296          let mut missing_chunks_list = missing_chunks
297            .iter()
298            .map(|&chunk| {
299              let chunk = chunk_by_ukey.expect_get(chunk);
300              chunk.name().unwrap_or("unnamed chunk(s)")
301            })
302            .collect::<Vec<_>>();
303          missing_chunks_list.sort_unstable();
304
305          let mut chunks = chunk_graph
306            .get_module_chunks(*module_id)
307            .iter()
308            .map(|&chunk| {
309              let chunk = chunk_by_ukey.expect_get(&chunk);
310              chunk.name().unwrap_or("unnamed chunk(s)")
311            })
312            .collect::<Vec<_>>();
313          chunks.sort_unstable();
314
315          format!(
316            "Module {} is not in the same chunk(s) (expected in chunk(s) {}, module is in chunk(s) {})",
317            module_readable_identifier,
318            missing_chunks_list.join(", "),
319            chunks.join(", ")
320          )
321        };
322
323        statistics.incorrect_chunks += 1;
324        let problem = Warning::Problem(problem_string);
325        failure_cache.insert(*module_id, problem.clone());
326        return Some(problem);
327      }
328
329      let NoRuntimeModuleCache {
330        incomings,
331        active_incomings,
332        runtime: cached_module_runtime,
333        ..
334      } = module_cache
335        .get(module_id)
336        .expect("should have module cache");
337
338      if let Some(incoming_connections_from_non_modules) = incomings.get(&None) {
339        let has_active_non_modules_connections =
340          incoming_connections_from_non_modules
341            .iter()
342            .any(|connection| {
343              is_connection_active_in_runtime(
344                connection,
345                runtime,
346                active_incomings,
347                cached_module_runtime,
348                module_graph,
349                module_graph_cache,
350              )
351            });
352
353        // TODO: ADD module connection explanations
354        if has_active_non_modules_connections {
355          let problem = {
356            // let importing_explanations = active_non_modules_connections
357            //   .iter()
358            //   .flat_map(|&c| c.explanation())
359            //   .collect::<HashSet<_>>();
360            // let mut explanations: Vec<_> = importing_explanations.into_iter().collect();
361            // explanations.sort();
362            format!(
363              "Module {module_readable_identifier} is referenced",
364              // if !explanations.is_empty() {
365              //   format!("by: {}", explanations.join(", "))
366              // } else {
367              //   "in an unsupported way".to_string()
368              // }
369            )
370          };
371          let problem = Warning::Problem(problem);
372          statistics.incorrect_dependency += 1;
373          failure_cache.insert(*module_id, problem.clone());
374          return Some(problem);
375        }
376      }
377
378      let mut incoming_connections_from_modules = HashMap::default();
379      for (origin_module, connections) in incomings.iter() {
380        if let Some(origin_module) = origin_module {
381          let number_of_chunks = module_cache
382            .get(origin_module)
383            .map(|m| m.number_of_chunks)
384            .unwrap_or_else(|| chunk_graph.get_number_of_module_chunks(*origin_module));
385
386          if number_of_chunks == 0 {
387            // Ignore connection from orphan modules
388            continue;
389          }
390
391          let is_intersect = if let Some(runtime) = runtime {
392            if let Some(origin_runtime) = module_cache.get(origin_module).map(|m| &m.runtime) {
393              !runtime.is_disjoint(origin_runtime)
394            } else {
395              let mut origin_runtime = RuntimeSpec::default();
396              for r in chunk_graph.get_module_runtimes_iter(*origin_module, chunk_by_ukey) {
397                origin_runtime.extend(r);
398              }
399              !runtime.is_disjoint(&origin_runtime)
400            }
401          } else {
402            false
403          };
404
405          if !is_intersect {
406            continue;
407          }
408
409          let active_connections: Vec<_> = connections
410            .iter()
411            .filter(|&connection| {
412              is_connection_active_in_runtime(
413                connection,
414                runtime,
415                active_incomings,
416                cached_module_runtime,
417                module_graph,
418                module_graph_cache,
419              )
420            })
421            .collect();
422
423          if !active_connections.is_empty() {
424            incoming_connections_from_modules.insert(origin_module, active_connections);
425          }
426        }
427      }
428
429      let mut incoming_modules = incoming_connections_from_modules
430        .keys()
431        .map(|mid| **mid)
432        .collect::<Vec<_>>();
433      let other_chunk_modules = incoming_modules
434        .iter()
435        .filter(|&origin_module| {
436          chunk_graph
437            .get_module_chunks(config.root_module)
438            .iter()
439            .any(|&chunk_ukey| !chunk_graph.is_module_in_chunk(origin_module, chunk_ukey))
440        })
441        .collect::<Vec<_>>();
442
443      if !other_chunk_modules.is_empty() {
444        let problem = {
445          let mut names: Vec<_> = other_chunk_modules
446            .into_iter()
447            .map(|mid| {
448              get_cached_readable_identifier(
449                mid,
450                module_graph,
451                &compilation.module_static_cache_artifact,
452                &compilation.options.context,
453              )
454            })
455            .collect();
456          names.sort();
457          format!(
458            "Module {} is referenced from different chunks by these modules: {}",
459            module_readable_identifier,
460            names.join(", ")
461          )
462        };
463
464        statistics.incorrect_chunks_of_importer += 1;
465        let problem = Warning::Problem(problem);
466        failure_cache.insert(*module_id, problem.clone());
467        return Some(problem);
468      }
469
470      let mut non_esm_connections = HashMap::default();
471      for (origin_module, connections) in incoming_connections_from_modules.iter() {
472        let has_non_esm_connections = connections.iter().any(|connection| {
473          let dep = module_graph.dependency_by_id(&connection.dependency_id);
474          !is_esm_dep_like(dep)
475        });
476
477        if has_non_esm_connections {
478          non_esm_connections.insert(origin_module, connections);
479        }
480      }
481
482      if !non_esm_connections.is_empty() {
483        let problem = {
484          let names: Vec<_> = non_esm_connections
485            .iter()
486            .map(|(origin_module, connections)| {
487              let readable_identifier = get_cached_readable_identifier(
488                origin_module,
489                module_graph,
490                &compilation.module_static_cache_artifact,
491                &compilation.options.context,
492              );
493              let mut names = connections
494                .iter()
495                .map(|item| {
496                  let dep = module_graph.dependency_by_id(&item.dependency_id);
497                  dep.dependency_type().to_string()
498                })
499                .collect::<Vec<_>>();
500              names.sort();
501              format!(
502                "{} (referenced with {})",
503                readable_identifier,
504                names.join(",")
505              )
506            })
507            .collect();
508
509          format!(
510            "Module {} is referenced from these modules with unsupported syntax: {}",
511            module_readable_identifier,
512            names.join(", ")
513          )
514        };
515        let problem = Warning::Problem(problem);
516        statistics.incorrect_module_dependency += 1;
517        failure_cache.insert(*module_id, problem.clone());
518        return Some(problem);
519      }
520
521      if let Some(runtime) = runtime
522        && runtime.len() > 1
523      {
524        let mut other_runtime_connections = Vec::new();
525        'outer: for (origin_module, connections) in incoming_connections_from_modules {
526          let mut current_runtime_condition = RuntimeCondition::Boolean(false);
527          for connection in connections {
528            let runtime_condition = filter_runtime(Some(runtime), |runtime| {
529              connection.is_target_active(module_graph, runtime, module_graph_cache)
530            });
531
532            if runtime_condition == RuntimeCondition::Boolean(false) {
533              continue;
534            }
535
536            if runtime_condition == RuntimeCondition::Boolean(true) {
537              continue 'outer;
538            }
539
540            // here two runtime_condition must be `RuntimeCondition::Spec`
541            if current_runtime_condition != RuntimeCondition::Boolean(false) {
542              current_runtime_condition
543                .as_spec_mut()
544                .expect("should be spec")
545                .extend(runtime_condition.as_spec().expect("should be spec"));
546            } else {
547              current_runtime_condition = runtime_condition;
548            }
549          }
550
551          if current_runtime_condition != RuntimeCondition::Boolean(false) {
552            other_runtime_connections.push((origin_module, current_runtime_condition));
553          }
554        }
555
556        if !other_runtime_connections.is_empty() {
557          let problem = {
558            format!(
559              "Module {} is runtime-dependent referenced by these modules: {}",
560              module_readable_identifier,
561              other_runtime_connections
562                .iter()
563                .map(|(origin_module, runtime_condition)| {
564                  let readable_identifier = get_cached_readable_identifier(
565                    origin_module,
566                    module_graph,
567                    &compilation.module_static_cache_artifact,
568                    &compilation.options.context,
569                  );
570                  format!(
571                    "{} (expected runtime {}, module is only referenced in {})",
572                    readable_identifier,
573                    runtime,
574                    runtime_condition.as_spec().expect("should be spec")
575                  )
576                })
577                .collect::<Vec<_>>()
578                .join(", ")
579            )
580          };
581
582          let problem = Warning::Problem(problem);
583          statistics.incorrect_runtime_condition += 1;
584          failure_cache.insert(*module_id, problem.clone());
585          return Some(problem);
586        }
587      }
588
589      incoming_modules.sort();
590      success_cache.insert(*module_id, runtime, incoming_modules.clone());
591      incoming_modules
592    };
593
594    let backup = if avoid_mutate_on_failure {
595      Some(config.snapshot())
596    } else {
597      None
598    };
599
600    config.add(*module_id);
601
602    for origin_module in &incoming_modules {
603      if let Some(problem) = Self::try_to_add(
604        compilation,
605        config,
606        origin_module,
607        runtime,
608        active_runtime,
609        possible_modules,
610        candidates,
611        failure_cache,
612        success_cache,
613        false,
614        statistics,
615        imports_cache,
616        module_cache,
617      ) {
618        if let Some(backup) = &backup {
619          config.rollback(*backup);
620        }
621        statistics.importer_failed += 1;
622        failure_cache.insert(*module_id, problem.clone());
623        return Some(problem);
624      }
625    }
626
627    for imp in Self::get_imports(
628      module_graph,
629      module_graph_cache,
630      *module_id,
631      runtime,
632      imports_cache,
633      module_cache,
634    ) {
635      candidates.insert(imp);
636    }
637    statistics.added += 1;
638    None
639  }
640
641  pub async fn process_concatenated_configuration(
642    compilation: &mut Compilation,
643    config: ConcatConfiguration,
644    used_modules: &mut HashSet<ModuleIdentifier>,
645  ) -> Result<()> {
646    let module_graph = compilation.get_module_graph();
647
648    let root_module_id = config.root_module;
649    if used_modules.contains(&root_module_id) {
650      return Ok(());
651    }
652
653    let modules_set = config.get_modules();
654    for m in modules_set {
655      used_modules.insert(*m);
656    }
657    let box_module = module_graph
658      .module_by_identifier(&root_module_id)
659      .expect("should have module");
660    let root_module_source_types = box_module.source_types(module_graph);
661
662    let is_root_module_asset_module = root_module_source_types.contains(&SourceType::Asset);
663    if is_root_module_asset_module && !root_module_source_types.contains(&SourceType::JavaScript) {
664      return Ok(());
665    }
666
667    let root_module_ctxt = RootModuleContext {
668      id: root_module_id,
669      readable_identifier: get_cached_readable_identifier(
670        &root_module_id,
671        module_graph,
672        &compilation.module_static_cache_artifact,
673        &compilation.options.context,
674      ),
675      name_for_condition: box_module.name_for_condition().clone(),
676      lib_indent: box_module
677        .lib_ident(LibIdentOptions {
678          context: compilation.options.context.as_str(),
679        })
680        .map(|id| id.to_string()),
681      layer: box_module.get_layer().cloned(),
682      resolve_options: box_module.get_resolve_options().clone(),
683      code_generation_dependencies: box_module
684        .get_code_generation_dependencies()
685        .map(|deps| deps.to_vec()),
686      presentational_dependencies: box_module
687        .get_presentational_dependencies()
688        .map(|deps| deps.to_vec()),
689      context: Some(compilation.options.context.clone()),
690      side_effect_connection_state: box_module.get_side_effects_connection_state(
691        module_graph,
692        &compilation.module_graph_cache_artifact,
693        &mut IdentifierSet::default(),
694        &mut IdentifierMap::default(),
695      ),
696      factory_meta: box_module.factory_meta().cloned(),
697      build_meta: box_module.build_meta().clone(),
698      module_argument: box_module.get_module_argument(),
699      exports_argument: box_module.get_exports_argument(),
700    };
701    let modules = modules_set
702      .iter()
703      .map(|id| {
704        let module = module_graph
705          .module_by_identifier(id)
706          .unwrap_or_else(|| panic!("should have module {id}"));
707
708        ConcatenatedInnerModule {
709          id: *id,
710          size: module.size(
711            Some(&rspack_core::SourceType::JavaScript),
712            Some(compilation),
713          ),
714          shorten_id: get_cached_readable_identifier(
715            id,
716            module_graph,
717            &compilation.module_static_cache_artifact,
718            &compilation.options.context,
719          ),
720        }
721      })
722      .collect::<Vec<_>>();
723    let mut new_module = ConcatenatedModule::create(
724      root_module_ctxt,
725      modules,
726      Some(rspack_hash::HashFunction::MD4),
727      config.runtime.clone(),
728      compilation,
729    );
730    new_module
731      .build(
732        rspack_core::BuildContext {
733          compiler_id: compilation.compiler_id(),
734          compilation_id: compilation.id(),
735          resolver_factory: compilation.resolver_factory.clone(),
736          plugin_driver: compilation.plugin_driver.clone(),
737          compiler_options: compilation.options.clone(),
738          fs: compilation.input_filesystem.clone(),
739          runtime_template: compilation.runtime_template.clone_without_dojang(),
740        },
741        Some(compilation),
742      )
743      .await?;
744    let mut chunk_graph = std::mem::take(&mut compilation.chunk_graph);
745    let module_graph = compilation.get_module_graph_mut();
746    let root_mgm_exports = module_graph
747      .module_graph_module_by_identifier(&root_module_id)
748      .expect("should have mgm")
749      .exports;
750    let module_graph_module = ModuleGraphModule::new(new_module.id(), root_mgm_exports);
751    module_graph.add_module_graph_module(module_graph_module);
752    ModuleGraph::clone_module_attributes(compilation, &root_module_id, &new_module.id());
753    // integrate
754
755    let module_graph = compilation.get_module_graph_mut();
756    for m in modules_set {
757      if m == &root_module_id {
758        continue;
759      }
760      module_graph.copy_outgoing_module_connections(m, &new_module.id(), |con, dep| {
761        con.original_module_identifier.as_ref() == Some(m)
762          && !(is_esm_dep_like(dep) && modules_set.contains(con.module_identifier()))
763      });
764      // TODO: optimize asset module https://github.com/webpack/webpack/pull/15515/files
765      for chunk_ukey in chunk_graph.get_module_chunks(root_module_id).clone() {
766        let module = module_graph
767          .module_by_identifier(m)
768          .expect("should exist module");
769
770        let source_types =
771          chunk_graph.get_chunk_module_source_types(&chunk_ukey, module, module_graph);
772
773        if source_types.len() == 1
774          && !matches!(
775            source_types.iter().next().expect("has length"),
776            SourceType::Css
777          )
778        {
779          chunk_graph.disconnect_chunk_and_module(&chunk_ukey, *m);
780        } else {
781          let new_source_types = source_types
782            .into_iter()
783            .filter(|source_type| !matches!(source_type, SourceType::JavaScript))
784            .collect();
785          chunk_graph.set_chunk_modules_source_types(&chunk_ukey, *m, new_source_types)
786        }
787      }
788    }
789
790    // different from webpack
791    // Rspack: if entry is an asset module, outputs a js chunk and a asset chunk
792    // Webpack: if entry is an asset module, outputs an asset chunk
793    // these lines of codes fix a bug: when asset module (NormalModule) is concatenated into ConcatenatedModule, the asset will be lost
794    // 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
795    if is_root_module_asset_module {
796      chunk_graph.replace_module(&root_module_id, &new_module.id());
797      chunk_graph.add_module(root_module_id);
798      for chunk_ukey in chunk_graph.get_module_chunks(new_module.id()).clone() {
799        let module = module_graph
800          .module_by_identifier(&root_module_id)
801          .expect("should exist module");
802
803        let source_types =
804          chunk_graph.get_chunk_module_source_types(&chunk_ukey, module, module_graph);
805        let new_source_types = source_types
806          .iter()
807          .filter(|source_type| !matches!(source_type, SourceType::JavaScript))
808          .copied()
809          .collect();
810        chunk_graph.set_chunk_modules_source_types(&chunk_ukey, root_module_id, new_source_types);
811        chunk_graph.connect_chunk_and_module(chunk_ukey, root_module_id);
812      }
813    } else {
814      chunk_graph.replace_module(&root_module_id, &new_module.id());
815    }
816
817    module_graph.move_module_connections(&root_module_id, &new_module.id(), |c, dep| {
818      let other_module = if *c.module_identifier() == root_module_id {
819        c.original_module_identifier
820      } else {
821        Some(*c.module_identifier())
822      };
823      let inner_connection = is_esm_dep_like(dep)
824        && if let Some(other_module) = other_module {
825          modules_set.contains(&other_module)
826        } else {
827          false
828        };
829      !inner_connection
830    });
831    module_graph.add_module(new_module.boxed());
832    compilation.chunk_graph = chunk_graph;
833    Ok(())
834  }
835
836  async fn optimize_chunk_modules_impl(&self, compilation: &mut Compilation) -> Result<()> {
837    let logger = compilation.get_logger("rspack.ModuleConcatenationPlugin");
838
839    if compilation.options.experiments.defer_import {
840      let mut imported_by_defer_modules_artifact = ImportedByDeferModulesArtifact::default();
841      let module_graph = compilation.get_module_graph();
842      for dep in module_graph.dependencies().values() {
843        if dep.get_phase().is_defer()
844          && matches!(
845            dep.dependency_type(),
846            DependencyType::EsmImport | DependencyType::EsmExportImport
847          )
848          && let Some(module) = module_graph.module_identifier_by_dependency_id(dep.id())
849        {
850          imported_by_defer_modules_artifact.insert(*module);
851        }
852      }
853      compilation.imported_by_defer_modules_artifact = imported_by_defer_modules_artifact;
854    }
855
856    let mut relevant_modules = vec![];
857    let mut possible_inners = IdentifierSet::default();
858    let start = logger.time("select relevant modules");
859    let module_graph = compilation.get_module_graph();
860
861    // filter modules that can be root
862    let modules: Vec<_> = module_graph
863      .module_graph_modules()
864      .keys()
865      .copied()
866      .collect();
867    let res: Vec<_> = modules
868      .into_par_iter()
869      .map(|module_id| {
870        let mut can_be_root = true;
871        let mut can_be_inner = true;
872        let mut bailout_reason = vec![];
873        let number_of_module_chunks = compilation
874          .chunk_graph
875          .get_number_of_module_chunks(module_id);
876        let is_entry_module = compilation.chunk_graph.is_entry_module(&module_id);
877        let module_graph = compilation.get_module_graph();
878        let m = module_graph
879          .module_by_identifier(&module_id)
880          .expect("should have module");
881
882        if let Some(reason) =
883          m.get_concatenation_bailout_reason(module_graph, &compilation.chunk_graph)
884        {
885          bailout_reason.push(reason);
886          return (false, false, module_id, bailout_reason);
887        }
888
889        if ModuleGraph::is_async(&compilation.async_modules_artifact.borrow(), &module_id) {
890          bailout_reason.push("Module is async".into());
891          return (false, false, module_id, bailout_reason);
892        }
893
894        if !m.build_info().strict {
895          bailout_reason.push("Module is not in strict mode".into());
896          return (false, false, module_id, bailout_reason);
897        }
898        if number_of_module_chunks == 0 {
899          bailout_reason.push("Module is not in any chunk".into());
900          return (false, false, module_id, bailout_reason);
901        }
902
903        let exports_info =
904          module_graph.get_prefetched_exports_info(&module_id, PrefetchExportsInfoMode::Default);
905        let relevant_exports = exports_info.get_relevant_exports(None);
906        let unknown_exports = relevant_exports
907          .iter()
908          .filter(|export_info| {
909            export_info.is_reexport()
910              && !matches!(
911                get_target(
912                  export_info,
913                  module_graph,
914                  Rc::new(|_| true),
915                  &mut Default::default()
916                ),
917                Some(GetTargetResult::Target(_))
918              )
919          })
920          .copied()
921          .collect::<Vec<_>>();
922        if !unknown_exports.is_empty() {
923          let cur_bailout_reason = unknown_exports
924            .into_iter()
925            .map(|export_info| {
926              let name = export_info
927                .name()
928                .map(|name| name.to_string())
929                .unwrap_or("other exports".to_string());
930              format!("{} : {}", name, export_info.get_used_info())
931            })
932            .collect::<Vec<String>>()
933            .join(", ");
934          // self.set_bailout_reason(
935          //   &module_id,
936          //   format!("Reexports in this module do not have a static target ({bailout_reason})"),
937          //   &mut module_graph,
938          // );
939
940          bailout_reason.push(
941            format!("Reexports in this module do not have a static target ({cur_bailout_reason})")
942              .into(),
943          );
944
945          return (false, false, module_id, bailout_reason);
946        }
947        let unknown_provided_exports = relevant_exports
948          .iter()
949          .filter(|export_info| !matches!(export_info.provided(), Some(ExportProvided::Provided)))
950          .copied()
951          .collect::<Vec<_>>();
952
953        if !unknown_provided_exports.is_empty() {
954          let cur_bailout_reason = unknown_provided_exports
955            .into_iter()
956            .map(|export_info| {
957              let name = export_info
958                .name()
959                .map(|name| name.to_string())
960                .unwrap_or("other exports".to_string());
961              format!(
962                "{} : {} and {}",
963                name,
964                export_info.get_provided_info(),
965                export_info.get_used_info(),
966              )
967            })
968            .collect::<Vec<String>>()
969            .join(", ");
970          // self.set_bailout_reason(
971          //   &module_id,
972          //   format!("List of module exports is dynamic ({bailout_reason})"),
973          //   &mut module_graph,
974          // );
975          bailout_reason
976            .push(format!("List of module exports is dynamic ({cur_bailout_reason})").into());
977          can_be_root = false;
978        }
979
980        if is_entry_module {
981          // self.set_bailout_reason(
982          //   &module_id,
983          //   "Module is an entry point".to_string(),
984          //   &mut module_graph,
985          // );
986          can_be_inner = false;
987          bailout_reason.push("Module is an entry point".into());
988        }
989
990        if module_graph.is_deferred(&compilation.imported_by_defer_modules_artifact, &module_id) {
991          bailout_reason.push("Module is deferred".into());
992          can_be_inner = false;
993        }
994
995        (can_be_root, can_be_inner, module_id, bailout_reason)
996        // if can_be_root {
997        //   relevant_modules.push(module_id);
998        // }
999        // if can_be_inner {
1000        //   possible_inners.insert(module_id);
1001        // }
1002      })
1003      .collect();
1004
1005    let module_graph = compilation.get_module_graph_mut();
1006
1007    for (can_be_root, can_be_inner, module_id, bailout_reason) in res {
1008      if can_be_root {
1009        relevant_modules.push(module_id);
1010      }
1011      if can_be_inner {
1012        possible_inners.insert(module_id);
1013      }
1014      for bailout_reason in bailout_reason {
1015        self.set_bailout_reason(&module_id, bailout_reason, module_graph);
1016      }
1017    }
1018
1019    let module_graph = compilation.get_module_graph();
1020    logger.time_end(start);
1021    let mut relevant_len_buffer = itoa::Buffer::new();
1022    let relevant_len_str = relevant_len_buffer.format(relevant_modules.len());
1023    let mut possible_len_buffer = itoa::Buffer::new();
1024    let possible_len_str = possible_len_buffer.format(possible_inners.len());
1025    logger.debug(format!(
1026      "{} potential root modules, {} potential inner modules",
1027      relevant_len_str, possible_len_str,
1028    ));
1029
1030    let start = logger.time("sort relevant modules");
1031    relevant_modules.sort_by(|a, b| {
1032      let ad = module_graph.get_depth(a);
1033      let bd = module_graph.get_depth(b);
1034      ad.cmp(&bd)
1035    });
1036
1037    logger.time_end(start);
1038    let mut statistics = Statistics::default();
1039    let mut stats_candidates = 0;
1040    let mut stats_size_sum = 0;
1041    let mut stats_empty_configurations = 0;
1042
1043    let start = logger.time("find modules to concatenate");
1044    let mut concat_configurations: Vec<ConcatConfiguration> = Vec::new();
1045    let mut used_as_inner: IdentifierSet = IdentifierSet::default();
1046    let mut imports_cache = RuntimeIdentifierCache::<IdentifierIndexSet>::default();
1047
1048    let module_graph = compilation.get_module_graph();
1049    let module_graph_cache = &compilation.module_graph_cache_artifact;
1050    let module_static_cache_artifact = &compilation.module_static_cache_artifact;
1051    let compilation_context = &compilation.options.context;
1052    let cache_modules = relevant_modules
1053      .iter()
1054      .chain(possible_inners.iter())
1055      .copied()
1056      .collect::<IdentifierSet>();
1057    let modules_without_runtime_cache = cache_modules
1058      .into_par_iter()
1059      .map(|module_id| {
1060        let exports_info =
1061          module_graph.get_prefetched_exports_info(&module_id, PrefetchExportsInfoMode::Default);
1062        let provided_names = matches!(
1063          exports_info.get_provided_exports(),
1064          ProvidedExports::ProvidedNames(_)
1065        );
1066        let module = module_graph
1067          .module_by_identifier(&module_id)
1068          .expect("should have module");
1069        let mut runtime = RuntimeSpec::default();
1070        for r in compilation
1071          .chunk_graph
1072          .get_module_runtimes_iter(module_id, &compilation.chunk_by_ukey)
1073        {
1074          runtime.extend(r);
1075        }
1076
1077        let _ = get_cached_readable_identifier(
1078          &module_id,
1079          module_graph,
1080          module_static_cache_artifact,
1081          compilation_context,
1082        );
1083
1084        let connections = module
1085          .get_dependencies()
1086          .iter()
1087          .filter_map(|d| {
1088            let dep = module_graph.dependency_by_id(d);
1089            if !is_esm_dep_like(dep) {
1090              return None;
1091            }
1092            let con = module_graph.connection_by_dependency_id(d)?;
1093            let module_dep = dep.as_module_dependency().expect("should be module dep");
1094            let imported_names =
1095              module_dep.get_referenced_exports(module_graph, module_graph_cache, None);
1096
1097            Some((
1098              con.clone(),
1099              (
1100                imported_names.iter().all(|item| match item {
1101                  ExtendedReferencedExport::Array(arr) => !arr.is_empty(),
1102                  ExtendedReferencedExport::Export(export) => !export.name.is_empty(),
1103                }),
1104                con.is_target_active(module_graph, Some(&runtime), module_graph_cache),
1105              ),
1106            ))
1107          })
1108          .collect::<Vec<_>>();
1109
1110        let incomings: HashMap<Option<ModuleIdentifier>, Vec<ModuleGraphConnection>> = module_graph
1111          .get_incoming_connections_by_origin_module(&module_id)
1112          .into_iter()
1113          .map(|(k, v)| (k, v.into_iter().cloned().collect()))
1114          .collect();
1115        let mut active_incomings = HashMap::default();
1116        for connection in incomings.values().flatten() {
1117          active_incomings.insert(
1118            connection.dependency_id,
1119            connection.is_active(module_graph, Some(&runtime), module_graph_cache),
1120          );
1121        }
1122        let number_of_chunks = compilation
1123          .chunk_graph
1124          .get_number_of_module_chunks(module_id);
1125        (
1126          module_id,
1127          NoRuntimeModuleCache {
1128            runtime,
1129            provided_names,
1130            connections,
1131            incomings,
1132            active_incomings,
1133            number_of_chunks,
1134          },
1135        )
1136      })
1137      .collect::<HashMap<_, _>>();
1138
1139    for current_root in relevant_modules.iter() {
1140      if used_as_inner.contains(current_root) {
1141        continue;
1142      }
1143
1144      let NoRuntimeModuleCache { runtime, .. } = modules_without_runtime_cache
1145        .get(current_root)
1146        .expect("should have module");
1147      let module_graph = compilation.get_module_graph();
1148      let module_graph_cache = &compilation.module_graph_cache_artifact;
1149      let exports_info =
1150        module_graph.get_prefetched_exports_info(current_root, PrefetchExportsInfoMode::Default);
1151      let filtered_runtime = filter_runtime(Some(runtime), |r| exports_info.is_module_used(r));
1152      let active_runtime = match filtered_runtime {
1153        RuntimeCondition::Boolean(true) => Some(runtime.clone()),
1154        RuntimeCondition::Boolean(false) => None,
1155        RuntimeCondition::Spec(spec) => Some(spec),
1156      };
1157
1158      let mut current_configuration =
1159        ConcatConfiguration::new(*current_root, active_runtime.clone());
1160
1161      let mut failure_cache = IdentifierMap::default();
1162      let mut success_cache = RuntimeIdentifierCache::default();
1163      let mut candidates_visited = HashSet::default();
1164      let mut candidates = VecDeque::new();
1165
1166      let imports = Self::get_imports(
1167        module_graph,
1168        module_graph_cache,
1169        *current_root,
1170        active_runtime.as_ref(),
1171        &mut imports_cache,
1172        &modules_without_runtime_cache,
1173      );
1174      for import in imports {
1175        candidates.push_back(import);
1176      }
1177
1178      let mut import_candidates = IdentifierSet::default();
1179      while let Some(imp) = candidates.pop_front() {
1180        if candidates_visited.contains(&imp) {
1181          continue;
1182        } else {
1183          candidates_visited.insert(imp);
1184        }
1185        import_candidates.clear();
1186        match Self::try_to_add(
1187          compilation,
1188          &mut current_configuration,
1189          &imp,
1190          Some(runtime),
1191          active_runtime.as_ref(),
1192          &possible_inners,
1193          &mut import_candidates,
1194          &mut failure_cache,
1195          &mut success_cache,
1196          true,
1197          &mut statistics,
1198          &mut imports_cache,
1199          &modules_without_runtime_cache,
1200        ) {
1201          Some(problem) => {
1202            failure_cache.insert(imp, problem.clone());
1203            current_configuration.add_warning(imp, problem);
1204          }
1205          _ => {
1206            import_candidates.iter().for_each(|c: &ModuleIdentifier| {
1207              candidates.push_back(*c);
1208            });
1209          }
1210        }
1211      }
1212      stats_candidates += candidates.len();
1213      if !current_configuration.is_empty() {
1214        let modules = current_configuration.get_modules();
1215        stats_size_sum += modules.len();
1216        let root_module = current_configuration.root_module;
1217
1218        modules.iter().for_each(|module| {
1219          if *module != root_module {
1220            used_as_inner.insert(*module);
1221          }
1222        });
1223        concat_configurations.push(current_configuration);
1224      } else {
1225        stats_empty_configurations += 1;
1226        let module_graph = compilation.get_module_graph_mut();
1227        let optimization_bailouts = module_graph.get_optimization_bailout_mut(current_root);
1228        for warning in current_configuration.get_warnings_sorted() {
1229          optimization_bailouts.push(self.format_bailout_warning(warning.0, &warning.1));
1230        }
1231      }
1232    }
1233
1234    logger.time_end(start);
1235
1236    rayon::spawn(move || drop(modules_without_runtime_cache));
1237
1238    if !concat_configurations.is_empty() {
1239      let mut concat_len_buffer = itoa::Buffer::new();
1240      let concat_len_str = concat_len_buffer.format(concat_configurations.len());
1241      let mut avg_size_buffer = itoa::Buffer::new();
1242      let avg_size_str = avg_size_buffer.format(stats_size_sum / concat_configurations.len());
1243      let mut empty_configs_buffer = itoa::Buffer::new();
1244      let empty_configs_str = empty_configs_buffer.format(stats_empty_configurations);
1245      logger.debug(format!(
1246        "{} successful concat configurations (avg size: {}), {} bailed out completely",
1247        concat_len_str, avg_size_str, empty_configs_str
1248      ));
1249    }
1250
1251    let mut candidates_buffer = itoa::Buffer::new();
1252    let candidates_str = candidates_buffer.format(stats_candidates);
1253    let mut cached_buffer = itoa::Buffer::new();
1254    let cached_str = cached_buffer.format(statistics.cached);
1255    let mut already_in_config_buffer = itoa::Buffer::new();
1256    let already_in_config_str = already_in_config_buffer.format(statistics.already_in_config);
1257    let mut invalid_module_buffer = itoa::Buffer::new();
1258    let invalid_module_str = invalid_module_buffer.format(statistics.invalid_module);
1259    let mut incorrect_chunks_buffer = itoa::Buffer::new();
1260    let incorrect_chunks_str = incorrect_chunks_buffer.format(statistics.incorrect_chunks);
1261    let mut incorrect_dependency_buffer = itoa::Buffer::new();
1262    let incorrect_dependency_str =
1263      incorrect_dependency_buffer.format(statistics.incorrect_dependency);
1264    let mut incorrect_chunks_of_importer_buffer = itoa::Buffer::new();
1265    let incorrect_chunks_of_importer_str =
1266      incorrect_chunks_of_importer_buffer.format(statistics.incorrect_chunks_of_importer);
1267    let mut incorrect_module_dependency_buffer = itoa::Buffer::new();
1268    let incorrect_module_dependency_str =
1269      incorrect_module_dependency_buffer.format(statistics.incorrect_module_dependency);
1270    let mut incorrect_runtime_condition_buffer = itoa::Buffer::new();
1271    let incorrect_runtime_condition_str =
1272      incorrect_runtime_condition_buffer.format(statistics.incorrect_runtime_condition);
1273    let mut importer_failed_buffer = itoa::Buffer::new();
1274    let importer_failed_str = importer_failed_buffer.format(statistics.importer_failed);
1275    let mut added_buffer = itoa::Buffer::new();
1276    let added_str = added_buffer.format(statistics.added);
1277    logger.debug(format!(
1278        "{} 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)",
1279        candidates_str,
1280        cached_str,
1281        already_in_config_str,
1282        invalid_module_str,
1283        incorrect_chunks_str,
1284        incorrect_dependency_str,
1285        incorrect_chunks_of_importer_str,
1286        incorrect_module_dependency_str,
1287        incorrect_runtime_condition_str,
1288        importer_failed_str,
1289        added_str
1290    ));
1291
1292    // Copy from  https://github.com/webpack/webpack/blob/1f99ad6367f2b8a6ef17cce0e058f7a67fb7db18/lib/optimize/ModuleConcatenationPlugin.js#L368-L371
1293    // HACK: Sort configurations by length and start with the longest one
1294    // to get the biggest groups possible. Used modules are marked with usedModules
1295    // TODO(from webpack): Allow reusing existing configuration while trying to add dependencies.
1296    // This would improve performance. O(n^2) -> O(n)
1297    let start = logger.time("sort concat configurations");
1298    concat_configurations.sort_by(|a, b| b.modules.len().cmp(&a.modules.len()));
1299    logger.time_end(start);
1300
1301    let mut used_modules = HashSet::default();
1302    let mut batch = vec![];
1303
1304    for config in concat_configurations {
1305      if used_modules.contains(&config.root_module) {
1306        continue;
1307      }
1308      let modules_set = config.get_modules();
1309      used_modules.extend(modules_set.iter().copied());
1310      batch.push(config);
1311    }
1312
1313    let new_modules = rspack_futures::scope::<_, Result<_>>(|token| {
1314      batch.into_iter().for_each(|config| {
1315        let s = unsafe { token.used(&*compilation) };
1316        s.spawn(move |compilation| async move {
1317          let modules_set = config.get_modules();
1318          let new_module = create_concatenated_module(compilation, &config).await?;
1319          let new_module_id = new_module.id();
1320          let connections = prepare_concatenated_module_connections(
1321            compilation,
1322            &new_module_id,
1323            modules_set,
1324            |m, con, dep| {
1325              con.original_module_identifier.as_ref() == Some(m)
1326                && !(is_esm_dep_like(dep) && modules_set.contains(con.module_identifier()))
1327            },
1328          );
1329          let (root_outgoings, root_incomings) = prepare_concatenated_root_module_connections(
1330            compilation,
1331            &config.root_module,
1332            |m, c, dep| {
1333              let other_module = if c.module_identifier() == m {
1334                c.original_module_identifier
1335              } else {
1336                Some(*c.module_identifier())
1337              };
1338              let inner_connection = is_esm_dep_like(dep)
1339                && if let Some(other_module) = other_module {
1340                  modules_set.contains(&other_module)
1341                } else {
1342                  false
1343                };
1344              !inner_connection
1345            },
1346          );
1347          Ok((
1348            new_module,
1349            connections,
1350            root_outgoings,
1351            root_incomings,
1352            config,
1353          ))
1354        });
1355      });
1356    })
1357    .await
1358    .into_iter()
1359    .map(|r| r.to_rspack_result())
1360    .collect::<Result<Vec<_>>>()?;
1361
1362    let mut set_original_mid_tasks = vec![];
1363    let mut set_mid_tasks = vec![];
1364    let mut add_connection_tasks = vec![];
1365    let mut remove_connection_tasks = vec![];
1366
1367    for res in new_modules {
1368      let (new_module, outgoings, root_outgoings, root_incomings, config) = res?;
1369      let new_module_id = new_module.id();
1370      let root_module_id = config.root_module;
1371      add_concatenated_module(compilation, new_module, config);
1372
1373      for connection in outgoings.iter().chain(root_outgoings.iter()) {
1374        set_original_mid_tasks.push((*connection, new_module_id));
1375      }
1376      for connection in root_incomings.iter() {
1377        set_mid_tasks.push((*connection, new_module_id));
1378      }
1379      let mut all_outgoings = outgoings;
1380      all_outgoings.extend(root_outgoings.clone());
1381      add_connection_tasks.push((new_module_id, all_outgoings, root_incomings.clone()));
1382      remove_connection_tasks.push((root_module_id, root_outgoings, root_incomings));
1383    }
1384
1385    let module_graph = compilation.get_module_graph_mut();
1386    module_graph.batch_set_connections_original_module(set_original_mid_tasks);
1387    module_graph.batch_set_connections_module(set_mid_tasks);
1388    module_graph.batch_add_connections(add_connection_tasks);
1389    module_graph.batch_remove_connections(remove_connection_tasks);
1390
1391    Ok(())
1392  }
1393}
1394
1395#[plugin_hook(CompilationOptimizeChunkModules for ModuleConcatenationPlugin)]
1396async fn optimize_chunk_modules(&self, compilation: &mut Compilation) -> Result<Option<bool>> {
1397  if let Some(diagnostic) = compilation.incremental.disable_passes(
1398    IncrementalPasses::MODULES_HASHES
1399    | IncrementalPasses::MODULE_IDS
1400    | IncrementalPasses::CHUNK_IDS
1401    | IncrementalPasses::CHUNKS_RUNTIME_REQUIREMENTS
1402    | IncrementalPasses::CHUNKS_HASHES,
1403    "ModuleConcatenationPlugin (optimization.concatenateModules = true)",
1404    "it requires calculating the modules that can be concatenated based on all the modules, which is a global effect",
1405  ) && let Some(diagnostic) = diagnostic {
1406      compilation.push_diagnostic(diagnostic);
1407  }
1408
1409  self.optimize_chunk_modules_impl(compilation).await?;
1410
1411  Ok(None)
1412}
1413
1414impl Plugin for ModuleConcatenationPlugin {
1415  fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
1416    ctx
1417      .compilation_hooks
1418      .optimize_chunk_modules
1419      .tap(optimize_chunk_modules::new(self));
1420    Ok(())
1421  }
1422}
1423
1424#[derive(Debug, Default)]
1425struct Statistics {
1426  cached: u32,
1427  already_in_config: u32,
1428  invalid_module: u32,
1429  incorrect_chunks: u32,
1430  incorrect_dependency: u32,
1431  incorrect_module_dependency: u32,
1432  incorrect_chunks_of_importer: u32,
1433  incorrect_runtime_condition: u32,
1434  importer_failed: u32,
1435  cache_hit: u32,
1436  module_visit: IdentifierMap<usize>,
1437  added: u32,
1438}
1439
1440#[derive(Debug)]
1441pub struct NoRuntimeModuleCache {
1442  runtime: RuntimeSpec,
1443  provided_names: bool,
1444  connections: Vec<(ModuleGraphConnection, (bool, bool))>,
1445  incomings: HashMap<Option<ModuleIdentifier>, Vec<ModuleGraphConnection>>,
1446  active_incomings: HashMap<DependencyId, bool>,
1447  number_of_chunks: usize,
1448}
1449
1450async fn create_concatenated_module(
1451  compilation: &Compilation,
1452  config: &ConcatConfiguration,
1453) -> Result<ConcatenatedModule> {
1454  let module_graph = compilation.get_module_graph();
1455  let root_module_id = config.root_module;
1456  let modules_set = config.get_modules();
1457
1458  let box_module = module_graph
1459    .module_by_identifier(&root_module_id)
1460    .expect("should have module");
1461
1462  let root_module_ctxt = RootModuleContext {
1463    id: root_module_id,
1464    readable_identifier: get_cached_readable_identifier(
1465      &root_module_id,
1466      module_graph,
1467      &compilation.module_static_cache_artifact,
1468      &compilation.options.context,
1469    ),
1470    name_for_condition: box_module.name_for_condition().clone(),
1471    lib_indent: box_module
1472      .lib_ident(LibIdentOptions {
1473        context: compilation.options.context.as_str(),
1474      })
1475      .map(|id| id.to_string()),
1476    layer: box_module.get_layer().cloned(),
1477    resolve_options: box_module.get_resolve_options().clone(),
1478    code_generation_dependencies: box_module
1479      .get_code_generation_dependencies()
1480      .map(|deps| deps.to_vec()),
1481    presentational_dependencies: box_module
1482      .get_presentational_dependencies()
1483      .map(|deps| deps.to_vec()),
1484    context: Some(compilation.options.context.clone()),
1485    side_effect_connection_state: box_module.get_side_effects_connection_state(
1486      module_graph,
1487      &compilation.module_graph_cache_artifact,
1488      &mut IdentifierSet::default(),
1489      &mut IdentifierMap::default(),
1490    ),
1491    factory_meta: box_module.factory_meta().cloned(),
1492    build_meta: box_module.build_meta().clone(),
1493    module_argument: box_module.get_module_argument(),
1494    exports_argument: box_module.get_exports_argument(),
1495  };
1496  let modules = modules_set
1497    .iter()
1498    .map(|id| {
1499      let module = module_graph
1500        .module_by_identifier(id)
1501        .unwrap_or_else(|| panic!("should have module {id}"));
1502
1503      ConcatenatedInnerModule {
1504        id: *id,
1505        size: module.size(
1506          Some(&rspack_core::SourceType::JavaScript),
1507          Some(compilation),
1508        ),
1509        shorten_id: get_cached_readable_identifier(
1510          id,
1511          module_graph,
1512          &compilation.module_static_cache_artifact,
1513          &compilation.options.context,
1514        ),
1515      }
1516    })
1517    .collect::<Vec<_>>();
1518  let mut new_module = ConcatenatedModule::create(
1519    root_module_ctxt,
1520    modules,
1521    Some(rspack_hash::HashFunction::MD4),
1522    config.runtime.clone(),
1523    compilation,
1524  );
1525  new_module
1526    .build(
1527      rspack_core::BuildContext {
1528        compiler_id: compilation.compiler_id(),
1529        compilation_id: compilation.id(),
1530        resolver_factory: compilation.resolver_factory.clone(),
1531        plugin_driver: compilation.plugin_driver.clone(),
1532        compiler_options: compilation.options.clone(),
1533        fs: compilation.input_filesystem.clone(),
1534        runtime_template: compilation.runtime_template.clone_without_dojang(),
1535      },
1536      Some(compilation),
1537    )
1538    .await?;
1539
1540  Ok(new_module)
1541}
1542
1543fn prepare_concatenated_module_connections<F>(
1544  compilation: &Compilation,
1545  new_module: &ModuleIdentifier,
1546  modules_set: &IdentifierIndexSet,
1547  filter_connection: F,
1548) -> Vec<DependencyId>
1549where
1550  F: Fn(&ModuleIdentifier, &ModuleGraphConnection, &BoxDependency) -> bool,
1551{
1552  let mg = compilation.get_module_graph();
1553  let mut res = vec![];
1554  for m in modules_set.iter() {
1555    if m == new_module {
1556      continue;
1557    }
1558
1559    let old_mgm_connections = mg
1560      .module_graph_module_by_identifier(m)
1561      .expect("should have mgm")
1562      .outgoing_connections();
1563
1564    // Outgoing connections
1565    for dep_id in old_mgm_connections {
1566      let connection = mg
1567        .connection_by_dependency_id(dep_id)
1568        .expect("should have connection");
1569      let dep = mg.dependency_by_id(dep_id);
1570      if filter_connection(m, connection, dep) {
1571        res.push(*dep_id);
1572      }
1573    }
1574  }
1575  res
1576}
1577
1578fn prepare_concatenated_root_module_connections<F>(
1579  compilation: &Compilation,
1580  root_module_id: &ModuleIdentifier,
1581  filter_connection: F,
1582) -> (Vec<DependencyId>, Vec<DependencyId>)
1583where
1584  F: Fn(&ModuleIdentifier, &ModuleGraphConnection, &BoxDependency) -> bool,
1585{
1586  let mg = compilation.get_module_graph();
1587  let mut outgoings = vec![];
1588  let old_mgm_connections = mg
1589    .module_graph_module_by_identifier(root_module_id)
1590    .expect("should have mgm")
1591    .outgoing_connections();
1592
1593  for dep_id in old_mgm_connections {
1594    let connection = mg
1595      .connection_by_dependency_id(dep_id)
1596      .expect("should have connection");
1597
1598    let dep = mg.dependency_by_id(dep_id);
1599    if filter_connection(root_module_id, connection, dep) {
1600      outgoings.push(*dep_id);
1601    }
1602  }
1603
1604  let mut incomings = vec![];
1605  let incoming_connections = mg
1606    .module_graph_module_by_identifier(root_module_id)
1607    .expect("should have mgm")
1608    .incoming_connections();
1609
1610  for dep_id in incoming_connections {
1611    let connection = mg
1612      .connection_by_dependency_id(dep_id)
1613      .expect("should have connection");
1614    let dependency = mg.dependency_by_id(dep_id);
1615    if filter_connection(root_module_id, connection, dependency) {
1616      incomings.push(*dep_id);
1617    }
1618  }
1619
1620  (outgoings, incomings)
1621}
1622
1623fn add_concatenated_module(
1624  compilation: &mut Compilation,
1625  new_module: ConcatenatedModule,
1626  config: ConcatConfiguration,
1627) {
1628  let root_module_id = config.root_module;
1629  let modules_set = config.get_modules();
1630
1631  let module_graph = compilation.get_module_graph();
1632  let box_module = module_graph
1633    .module_by_identifier(&root_module_id)
1634    .expect("should have module");
1635  let root_module_source_types = box_module.source_types(module_graph);
1636  let is_root_module_asset_module = root_module_source_types.contains(&SourceType::Asset);
1637
1638  let mut chunk_graph = std::mem::take(&mut compilation.chunk_graph);
1639  let module_graph = compilation.get_module_graph_mut();
1640
1641  let root_mgm_exports = module_graph
1642    .module_graph_module_by_identifier(&root_module_id)
1643    .expect("should have mgm")
1644    .exports;
1645  let module_graph_module = ModuleGraphModule::new(new_module.id(), root_mgm_exports);
1646  module_graph.add_module_graph_module(module_graph_module);
1647  ModuleGraph::clone_module_attributes(compilation, &root_module_id, &new_module.id());
1648  // integrate
1649
1650  let module_graph = compilation.get_module_graph_mut();
1651
1652  for m in modules_set.iter() {
1653    if *m == root_module_id {
1654      continue;
1655    }
1656    let module = module_graph
1657      .module_by_identifier(m)
1658      .expect("should exist module");
1659    // TODO: optimize asset module https://github.com/webpack/webpack/pull/15515/files
1660    for chunk_ukey in chunk_graph.get_module_chunks(root_module_id).clone() {
1661      let source_types =
1662        chunk_graph.get_chunk_module_source_types(&chunk_ukey, module, module_graph);
1663
1664      if source_types.len() == 1 {
1665        chunk_graph.disconnect_chunk_and_module(&chunk_ukey, *m);
1666      } else {
1667        let new_source_types = source_types
1668          .into_iter()
1669          .filter(|source_type| !matches!(source_type, SourceType::JavaScript))
1670          .collect();
1671        chunk_graph.set_chunk_modules_source_types(&chunk_ukey, *m, new_source_types)
1672      }
1673    }
1674  }
1675
1676  // different from webpack
1677  // Rspack: if entry is an asset module, outputs a js chunk and a asset chunk
1678  // Webpack: if entry is an asset module, outputs an asset chunk
1679  // these lines of codes fix a bug: when asset module (NormalModule) is concatenated into ConcatenatedModule, the asset will be lost
1680  // 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
1681  if is_root_module_asset_module {
1682    chunk_graph.replace_module(&root_module_id, &new_module.id());
1683    chunk_graph.add_module(root_module_id);
1684    for chunk_ukey in chunk_graph.get_module_chunks(new_module.id()).clone() {
1685      let module = module_graph
1686        .module_by_identifier(&root_module_id)
1687        .expect("should exist module");
1688
1689      let source_types =
1690        chunk_graph.get_chunk_module_source_types(&chunk_ukey, module, module_graph);
1691      let new_source_types = source_types
1692        .iter()
1693        .filter(|source_type| !matches!(source_type, SourceType::JavaScript))
1694        .copied()
1695        .collect();
1696      chunk_graph.set_chunk_modules_source_types(&chunk_ukey, root_module_id, new_source_types);
1697      chunk_graph.connect_chunk_and_module(chunk_ukey, root_module_id);
1698    }
1699  } else {
1700    chunk_graph.replace_module(&root_module_id, &new_module.id());
1701  }
1702
1703  module_graph.add_module(new_module.boxed());
1704  compilation.chunk_graph = chunk_graph;
1705}
1706
1707fn is_connection_active_in_runtime(
1708  connection: &ModuleGraphConnection,
1709  runtime: Option<&RuntimeSpec>,
1710  cached_active_incomings: &HashMap<DependencyId, bool>,
1711  cached_runtime: &RuntimeSpec,
1712  mg: &ModuleGraph,
1713  mg_cache: &ModuleGraphCacheArtifact,
1714) -> bool {
1715  if let (Some(cached_active), Some(runtime)) = (
1716    cached_active_incomings.get(&connection.dependency_id),
1717    runtime,
1718  ) {
1719    if runtime == cached_runtime {
1720      return *cached_active;
1721    }
1722
1723    if *cached_active && cached_runtime.is_subset(runtime) {
1724      return true;
1725    }
1726
1727    if !*cached_active && cached_runtime.is_superset(runtime) {
1728      return false;
1729    }
1730  }
1731
1732  connection.is_active(mg, runtime, mg_cache)
1733}