1use std::fmt::Write;
2
3use anyhow::{Result, bail};
4use rustc_hash::FxHashMap;
5use swc_core::{
6 common::{DUMMY_SP, GLOBALS, SyntaxContext, comments::Comments, util::take::Take},
7 ecma::{
8 ast::{
9 ExportAll, ExportNamedSpecifier, Expr, ExprStmt, Id, Ident, ImportDecl, Lit, Module,
10 ModuleDecl, ModuleExportName, ModuleItem, NamedExport, Program, Stmt,
11 },
12 codegen::to_code,
13 },
14};
15use turbo_rcstr::RcStr;
16use turbo_tasks::{FxIndexSet, ResolvedVc, ValueToString, Vc, turbobail};
17use turbopack_core::{ident::AssetIdent, resolve::ModulePart, source::Source};
18
19use self::graph::{DepGraph, ItemData, ItemId, ItemIdGroupKind, Mode, SplitModuleResult};
20pub(crate) use self::graph::{
21 PartId, create_turbopack_part_id_assert, find_turbopack_part_id_in_asserts,
22};
23use crate::{
24 EcmascriptModuleAsset, EcmascriptParsable, analyzer::graph::EvalContext, parse::ParseResult,
25};
26
27mod graph;
28pub mod merge;
29mod optimizations;
30pub mod part;
31pub mod side_effects;
32#[cfg(test)]
33mod tests;
34mod util;
35
36pub(crate) const TURBOPACK_PART_IMPORT_SOURCE: &str = "__TURBOPACK_PART__";
37
38pub struct Analyzer<'a> {
39 g: &'a mut DepGraph,
40 item_ids: &'a Vec<ItemId>,
41 items: &'a mut FxHashMap<ItemId, ItemData>,
42
43 last_side_effects: Vec<ItemId>,
44
45 vars: FxHashMap<Id, VarState>,
46}
47
48#[derive(Debug, Default, Clone)]
49struct VarState {
50 declarator: Option<ItemId>,
51
52 last_writes: Vec<ItemId>,
56 last_reads: Vec<ItemId>,
58
59 last_op: Option<VarOp>,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63enum VarOp {
64 Read,
65 Write,
66}
67
68impl Analyzer<'_> {
69 pub(super) fn analyze(
70 module: &Module,
71 comments: &dyn Comments,
72 unresolved_ctxt: SyntaxContext,
73 top_level_ctxt: SyntaxContext,
74 ) -> (DepGraph, FxHashMap<ItemId, ItemData>) {
75 let mut g = DepGraph::default();
76 let (item_ids, mut items) = g.init(module, comments, unresolved_ctxt, top_level_ctxt);
77
78 let mut analyzer = Analyzer {
79 g: &mut g,
80 item_ids: &item_ids,
81 items: &mut items,
82 last_side_effects: Default::default(),
83 vars: Default::default(),
84 };
85
86 let eventual_ids = analyzer.hoist_vars_and_bindings();
87
88 analyzer.evaluate_immediate(module, &eventual_ids);
89
90 analyzer.evaluate_eventual(module);
91
92 analyzer.handle_exports(module);
93
94 analyzer.handle_explicit_deps();
95
96 (g, items)
97 }
98
99 fn handle_explicit_deps(&mut self) {
100 for item_id in self.item_ids.iter() {
101 if let Some(item) = self.items.get(item_id)
102 && !item.explicit_deps.is_empty()
103 {
104 self.g.add_strong_deps(item_id, item.explicit_deps.iter());
105 }
106 }
107 }
108
109 fn hoist_vars_and_bindings(&mut self) -> FxIndexSet<Id> {
114 let mut eventual_ids = FxIndexSet::default();
115
116 for item_id in self.item_ids.iter() {
117 if let Some(item) = self.items.get(item_id) {
118 eventual_ids.extend(item.eventual_read_vars.iter().cloned());
119 eventual_ids.extend(item.eventual_write_vars.iter().cloned());
120
121 if item.is_hoisted && item.side_effects {
122 self.g
123 .add_strong_deps(item_id, self.last_side_effects.last());
124
125 self.last_side_effects.push(item_id.clone());
126 }
127
128 for id in item.var_decls.iter() {
129 let state = self.vars.entry(id.clone()).or_default();
130
131 if state.declarator.is_none() {
132 state.declarator = Some(item_id.clone());
133 }
134
135 if item.is_hoisted {
136 state.last_writes.push(item_id.clone());
137 } else {
138 }
141 }
142 }
143 }
144
145 eventual_ids
146 }
147
148 fn evaluate_immediate(&mut self, _module: &Module, eventual_ids: &FxIndexSet<Id>) {
150 for item_id in self.item_ids.iter() {
151 if let Some(item) = self.items.get(item_id) {
152 if item.is_hoisted {
154 continue;
155 }
156
157 for id in item.var_decls.iter() {
158 let state = self.vars.entry(id.clone()).or_default();
159 if state.declarator.is_none() {
160 state.declarator = Some(item_id.clone());
161 }
162 }
163
164 for id in item.read_vars.iter() {
166 let state = self.vars.entry(id.clone()).or_default();
172 self.g.add_strong_deps(item_id, state.last_writes.iter());
173
174 if let Some(declarator) = &state.declarator
175 && declarator != item_id
176 {
177 self.g
179 .add_strong_deps(item_id, [declarator].iter().copied());
180 }
181
182 if state.last_op == Some(VarOp::Write) && !item.write_vars.contains(id) {
183 state.last_reads.clear();
184 }
185 }
186
187 for id in item.write_vars.iter() {
189 let state = self.vars.entry(id.clone()).or_default();
196 self.g.add_weak_deps(item_id, state.last_reads.iter());
197
198 if let Some(declarator) = &state.declarator
199 && declarator != item_id
200 {
201 self.g.add_strong_deps(item_id, [declarator]);
203 }
204
205 if !item.read_vars.contains(id) {
206 if state.last_op == Some(VarOp::Read) {
210 state.last_writes.clear();
211 } else if state.last_op == Some(VarOp::Write) {
212 }
214 } else {
215 if state.last_op.is_some() {
222 state.last_reads.clear();
223 state.last_writes.clear();
224 }
225 }
226 }
227
228 if item.side_effects {
229 self.g
232 .add_strong_deps(item_id, self.last_side_effects.last());
233
234 for id in eventual_ids.iter() {
242 let state = self.vars.entry(id.clone()).or_default();
243
244 self.g.add_weak_deps(item_id, state.last_writes.iter());
245 self.g.add_strong_deps(item_id, state.last_reads.iter());
246 }
247 }
248
249 for id in item.write_vars.iter() {
251 let state = self.vars.entry(id.clone()).or_default();
254 state.last_writes.push(item_id.clone());
255
256 state
260 .last_writes
261 .retain(|last_write| !self.g.has_dep(item_id, last_write, true));
262
263 state
275 .last_writes
276 .retain(|last_write| self.g.has_path_connecting(item_id, last_write));
277 }
278
279 for id in item.read_vars.iter() {
281 let state = self.vars.entry(id.clone()).or_default();
284 state.last_reads.push(item_id.clone());
285
286 state
290 .last_reads
291 .retain(|last_read| !self.g.has_dep(item_id, last_read, true));
292
293 state.last_op = Some(VarOp::Read);
294 }
295
296 for id in item.write_vars.iter() {
297 let state = self.vars.entry(id.clone()).or_default();
298 state.last_op = Some(VarOp::Write);
299 }
300
301 if item.side_effects {
302 self.last_side_effects.push(item_id.clone());
303 }
304 }
305 }
306 }
307
308 fn evaluate_eventual(&mut self, _module: &Module) {
310 for item_id in self.item_ids.iter() {
311 if let Some(item) = self.items.get(item_id) {
312 for id in item.eventual_read_vars.iter() {
315 let state = self.vars.entry(id.clone()).or_default();
319 self.g.add_strong_deps(item_id, state.last_writes.iter());
320
321 if let Some(declarator) = &state.declarator
322 && declarator != item_id
323 {
324 self.g.add_strong_deps(item_id, [declarator]);
326 }
327 }
328
329 for id in item.eventual_write_vars.iter() {
331 let state = self.vars.entry(id.clone()).or_default();
335
336 self.g.add_weak_deps(item_id, state.last_reads.iter());
337
338 if let Some(declarator) = &state.declarator
339 && declarator != item_id
340 {
341 self.g.add_strong_deps(item_id, [declarator]);
343 }
344 }
345
346 }
349 }
350 }
351
352 fn handle_exports(&mut self, _module: &Module) {
354 if let Some(last) = self.last_side_effects.last()
356 && let Some(item) = self.items.get_mut(last)
357 {
358 item.is_module_evaluation = true;
359 }
360
361 for item_id in self.item_ids.iter() {
362 if let ItemId::Group(ItemIdGroupKind::Export(local, _)) = item_id {
363 let state = self.vars.entry(local.clone()).or_default();
366
367 self.g.add_strong_deps(item_id, state.last_writes.iter());
368 }
369 }
370 }
371}
372
373#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
374pub(crate) enum Key {
375 ModuleEvaluation,
376 Export(RcStr),
377 Exports,
378 StarExports,
379}
380
381async fn get_part_id(result: &SplitResult, part: &ModulePart) -> Result<u32> {
383 let key = match part {
385 ModulePart::Evaluation => Key::ModuleEvaluation,
386 ModulePart::Export(export) | ModulePart::PartialExport { export, .. } => {
387 Key::Export(export.clone())
388 }
389 ModulePart::Exports => Key::Exports,
390 ModulePart::Internal(part_id) => return Ok(*part_id),
391 ModulePart::Locals
392 | ModulePart::Facade
393 | ModulePart::RenamedExport { .. }
394 | ModulePart::RenamedNamespace { .. }
395 | ModulePart::RenamedPartialNamespace { .. } => {
396 bail!("invalid module part")
397 }
398 };
399
400 let SplitResult::Ok {
401 entrypoints,
402 modules,
403 ..
404 } = &result
405 else {
406 bail!("split failed")
407 };
408
409 if let Some(id) = entrypoints.get(&key) {
410 return Ok(*id);
411 }
412
413 if part.get_export().is_some()
415 && let Some(&v) = entrypoints
416 .get(&Key::StarExports)
417 .or_else(|| entrypoints.get(&Key::Exports))
418 {
419 return Ok(v);
420 }
421
422 let mut dump = String::new();
423
424 for (idx, m) in modules.iter().enumerate() {
425 let ParseResult::Ok { program, .. } = &*m.await? else {
426 bail!("failed to get module")
427 };
428
429 {
430 let code = to_code(&program);
431
432 writeln!(dump, "# Module #{idx}:\n{code}\n\n\n")?;
433 }
434 }
435
436 bail!(
437 "could not find part id for module part {:?} in {:?}\n\nModule dump:\n{dump}",
438 key,
439 entrypoints
440 )
441}
442
443#[turbo_tasks::value(shared, serialization = "skip", eq = "manual")]
444pub(crate) enum SplitResult {
445 Ok {
446 asset_ident: ResolvedVc<AssetIdent>,
447
448 #[turbo_tasks(unsafe_ignore)]
450 entrypoints: FxHashMap<Key, u32>,
451
452 #[turbo_tasks(debug_ignore)]
453 modules: Vec<ResolvedVc<ParseResult>>,
454 #[turbo_tasks(unsafe_ignore)]
455 deps: FxHashMap<u32, Vec<PartId>>,
456 #[turbo_tasks(debug_ignore, unsafe_ignore)]
457 star_reexports: Vec<ExportAll>,
458 },
459 Failed {
460 parse_result: ResolvedVc<ParseResult>,
461 },
462}
463
464impl PartialEq for SplitResult {
465 fn eq(&self, other: &Self) -> bool {
466 match (self, other) {
467 (Self::Ok { .. }, Self::Ok { .. }) => false,
468 _ => core::mem::discriminant(self) == core::mem::discriminant(other),
469 }
470 }
471}
472
473#[turbo_tasks::function]
474pub(super) async fn split_module(asset: Vc<EcmascriptModuleAsset>) -> Result<Vc<SplitResult>> {
475 let parsed: ResolvedVc<ParseResult> = asset.failsafe_parse().to_resolved().await?;
476 let ident = asset.source().ident().to_resolved().await?;
477 if !ident.await?.parts.is_empty() {
479 return Ok(SplitResult::Failed {
480 parse_result: parsed,
481 }
482 .cell());
483 }
484
485 let name = ident.to_string().await?;
488 if name.ends_with(".cjs") {
489 return Ok(SplitResult::Failed {
490 parse_result: parsed,
491 }
492 .cell());
493 }
494
495 let parse_result = parsed.await?;
496
497 match &*parse_result {
498 ParseResult::Ok {
499 program,
500 comments,
501 eval_context,
502 source_map,
503 globals,
504 program_source,
505 ..
506 } => {
507 if util::should_skip_tree_shaking(program) {
509 return Ok(SplitResult::Failed {
510 parse_result: parsed,
511 }
512 .cell());
513 }
514
515 let module = match program {
516 Program::Module(module) => module,
517 Program::Script(..) => unreachable!("CJS is already handled"),
518 };
519
520 let directives = module
522 .body
523 .iter()
524 .take_while(|item| {
525 matches!(
526 item,
527 ModuleItem::Stmt(Stmt::Expr(ExprStmt {
528 expr: Expr::Lit(Lit::Str(..)),
529 ..
530 }))
531 )
532 })
533 .cloned()
534 .collect::<Vec<_>>();
535
536 let (mut dep_graph, items) = GLOBALS.set(globals, || {
537 Analyzer::analyze(
538 module,
539 comments,
540 SyntaxContext::empty().apply_mark(eval_context.unresolved_mark),
541 SyntaxContext::empty().apply_mark(eval_context.top_level_mark),
542 )
543 });
544
545 dep_graph.handle_weak(Mode::Production);
546
547 let SplitModuleResult {
548 entrypoints,
549 part_deps,
550 modules,
551 star_reexports,
552 } = dep_graph.split_module(&directives, &items);
553
554 assert_ne!(modules.len(), 0, "modules.len() == 0;\nModule: {module:?}",);
555
556 for &v in entrypoints.values() {
557 debug_assert!(
558 v < modules.len() as u32,
559 "Invalid entrypoint '{}' while there are only '{}' modules",
560 v,
561 modules.len()
562 );
563 }
564 let modules = modules
565 .into_iter()
566 .map(|module| {
567 let program = Program::Module(module);
568
569 let eval_context = GLOBALS.set(globals, || {
570 EvalContext::new(
571 Some(&program),
572 eval_context.unresolved_mark,
573 eval_context.top_level_mark,
574 eval_context.force_free_values.clone(),
575 None,
576 )
577 });
578
579 ParseResult::resolved_cell(ParseResult::Ok {
580 program,
581 globals: globals.clone(),
582 comments: comments.clone(),
583 source_map: source_map.clone(),
584 eval_context,
585 source_mapping_url: None,
586 program_source: program_source.clone(),
587 })
588 })
589 .collect();
590
591 Ok(SplitResult::Ok {
592 asset_ident: ident,
593 entrypoints,
594 deps: part_deps,
595 modules,
596 star_reexports,
597 }
598 .cell())
599 }
600
601 _ => Ok(SplitResult::Failed {
602 parse_result: parsed,
603 }
604 .cell()),
605 }
606}
607
608#[turbo_tasks::function]
609pub(crate) async fn part_of_module(
610 split_data: Vc<SplitResult>,
611 part: ModulePart,
612) -> Result<Vc<ParseResult>> {
613 let split_data = split_data.await?;
614
615 match &*split_data {
616 SplitResult::Ok {
617 asset_ident,
618 modules,
619 entrypoints,
620 deps,
621 star_reexports,
622 ..
623 } => {
624 debug_assert_ne!(modules.len(), 0, "modules.len() == 0");
625
626 if part == ModulePart::Facade {
627 if let ParseResult::Ok {
628 comments,
629 eval_context,
630 globals,
631 source_map,
632 program_source,
633 ..
634 } = &*modules[0].await?
635 {
636 let mut module = Module::dummy();
637
638 let mut export_names = entrypoints
639 .keys()
640 .filter_map(|key| {
641 if let Key::Export(v) = key {
642 Some(v.clone())
643 } else {
644 None
645 }
646 })
647 .collect::<Vec<_>>();
648 export_names.sort();
649
650 module
651 .body
652 .push(ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
653 span: DUMMY_SP,
654 specifiers: vec![],
655 src: Box::new(TURBOPACK_PART_IMPORT_SOURCE.into()),
656 type_only: false,
657 with: Some(Box::new(create_turbopack_part_id_assert(
658 PartId::ModuleEvaluation,
659 ))),
660 phase: Default::default(),
661 })));
662
663 let specifiers = export_names
664 .into_iter()
665 .map(|export_name| {
666 swc_core::ecma::ast::ExportSpecifier::Named(ExportNamedSpecifier {
667 span: DUMMY_SP,
668 orig: ModuleExportName::Ident(Ident::new(
669 export_name.as_str().into(),
670 DUMMY_SP,
671 Default::default(),
672 )),
673 exported: None,
674 is_type_only: false,
675 })
676 })
677 .collect::<Vec<_>>();
678
679 module
680 .body
681 .push(ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(
682 NamedExport {
683 span: DUMMY_SP,
684 specifiers,
685 src: Some(Box::new(TURBOPACK_PART_IMPORT_SOURCE.into())),
686 type_only: false,
687 with: Some(Box::new(create_turbopack_part_id_assert(
688 PartId::Exports,
689 ))),
690 },
691 )));
692
693 module.body.extend(star_reexports.iter().map(|export_all| {
694 ModuleItem::ModuleDecl(ModuleDecl::ExportAll(export_all.clone()))
695 }));
696
697 let program = Program::Module(module);
698 let eval_context = GLOBALS.set(globals, || {
699 EvalContext::new(
700 Some(&program),
701 eval_context.unresolved_mark,
702 eval_context.top_level_mark,
703 eval_context.force_free_values.clone(),
704 None,
705 )
706 });
707
708 return Ok(ParseResult::Ok {
709 program,
710 comments: comments.clone(),
711 eval_context,
712 globals: globals.clone(),
713 source_map: source_map.clone(),
714 source_mapping_url: None,
715 program_source: program_source.clone(),
716 }
717 .cell());
718 } else {
719 unreachable!()
720 }
721 }
722
723 let part_id = get_part_id(&split_data, &part).await?;
724
725 if part_id as usize >= modules.len() {
726 turbobail!(
727 "part_id is out of range: {part_id} >= {}; asset = {}; entrypoints = \
728 {entrypoints:?}: part_deps = {deps:?}",
729 modules.len(),
730 *asset_ident
731 );
732 }
733
734 Ok(*modules[part_id as usize])
735 }
736 SplitResult::Failed { parse_result } => Ok(**parse_result),
737 }
738}