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 *cached_active
202 } else if cached.runtime.is_subset(runtime) && *cached_active {
203 true
205 } else if cached.runtime.is_superset(runtime) && !*cached_active {
206 false
208 } else {
209 con.is_target_active(mg, Some(runtime), mg_cache)
211 }
212 } else {
213 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 if has_active_non_modules_connections {
343 let problem = {
344 format!(
351 "Module {module_readable_identifier} is referenced",
352 )
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 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 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 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 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 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 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 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 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 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 })
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 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}