Skip to main content

turbopack_core/module_graph/
mod.rs

1use std::{
2    collections::{BinaryHeap, VecDeque},
3    future::Future,
4    iter::FusedIterator,
5    ops::Deref,
6    sync::OnceLock,
7};
8
9use anyhow::{Context, Result, bail};
10use bincode::{Decode, Encode};
11use petgraph::{
12    Direction,
13    graph::{DiGraph, EdgeIndex, NodeIndex},
14    visit::{EdgeRef, IntoNodeReferences, NodeIndexable, Reversed},
15};
16use rustc_hash::{FxHashMap, FxHashSet};
17use serde::{Deserialize, Serialize};
18use tracing::{Instrument, Level, Span};
19use turbo_rcstr::{RcStr, rcstr};
20use turbo_tasks::{
21    CollectiblesSource, FxIndexMap, JoinIterExt, NonLocalValue, OperationVc, ReadRef, ResolvedVc,
22    TryFlatJoinIterExt, TryJoinIterExt, ValueToString, Vc,
23    debug::ValueDebugFormat,
24    graph::{AdjacencyMap, GraphTraversal, Visit, VisitControlFlow},
25    keyed::KeyedAccess,
26};
27use turbo_tasks_fs::FileSystemPath;
28
29use crate::{
30    chunk::{AsyncModuleInfo, ChunkingContext, ChunkingType, TracedMode},
31    issue::{ImportTracer, ImportTraces, Issue, IssueExt, IssueSeverity, analyze::AnalyzeIssue},
32    module::Module,
33    module_graph::{
34        async_module_info::{AsyncModulesInfo, compute_async_module_info},
35        binding_usage_info::{BindingUsageInfo, UnusedReferenceEdges},
36        chunk_group_info::{ChunkGroupEntry, ChunkGroupInfo, compute_chunk_group_info},
37        collect::{CollectedModules, collect_graph},
38        merged_modules::{MergedModuleInfo, compute_merged_modules},
39        module_batches::{ModuleBatchesGraph, compute_module_batches},
40        style_groups::{StyleGroups, StyleGroupsAlgorithm, StyleGroupsConfig},
41        style_groups_graph::compute_style_groups_graph,
42        style_groups_loose::compute_style_groups,
43        traced_di_graph::TracedDiGraph,
44    },
45    reference::{
46        ModuleReference, primary_chunkable_referenced_modules,
47        referenced_modules_and_affecting_sources,
48    },
49    resolve::BindingUsage,
50};
51
52pub mod async_module_info;
53pub mod binding_usage_info;
54pub mod chunk_group_info;
55pub mod collect;
56pub mod merged_modules;
57pub mod module_batch;
58pub(crate) mod module_batches;
59mod side_effect_module_info;
60pub mod style_groups;
61pub mod style_groups_graph;
62pub mod style_groups_loose;
63mod traced_di_graph;
64
65pub use self::module_batches::BatchingConfig;
66
67#[derive(
68    Debug, Copy, Clone, Eq, PartialOrd, Ord, Hash, PartialEq, Serialize, Deserialize, Encode, Decode,
69)]
70pub struct GraphNodeIndex {
71    graph_idx: u32,
72    #[bincode(with_serde)]
73    node_idx: NodeIndex,
74}
75impl GraphNodeIndex {
76    fn new(graph_idx: u32, node_idx: NodeIndex) -> Self {
77        Self {
78            graph_idx,
79            node_idx,
80        }
81    }
82}
83
84unsafe impl NonLocalValue for GraphNodeIndex {}
85
86#[derive(
87    Debug, Copy, Clone, Eq, PartialOrd, Ord, Hash, PartialEq, NonLocalValue, Encode, Decode,
88)]
89pub struct GraphEdgeIndex {
90    graph_idx: u32,
91    #[turbo_tasks(unsafe_ignore)]
92    #[bincode(with_serde)]
93    edge_idx: EdgeIndex,
94}
95
96impl GraphEdgeIndex {
97    fn new(graph_idx: u32, edge_idx: EdgeIndex) -> Self {
98        Self {
99            graph_idx,
100            edge_idx,
101        }
102    }
103}
104
105#[turbo_tasks::value]
106#[derive(Clone, Debug)]
107pub struct VisitedModules {
108    #[bincode(with = "turbo_bincode::indexmap")]
109    pub modules: FxIndexMap<ResolvedVc<Box<dyn Module>>, GraphNodeIndex>,
110    next_graph_idx: u32,
111}
112
113#[turbo_tasks::value_impl]
114impl VisitedModules {
115    #[turbo_tasks::function(operation)]
116    pub fn empty() -> Vc<Self> {
117        Self {
118            modules: Default::default(),
119            next_graph_idx: 0,
120        }
121        .cell()
122    }
123
124    #[turbo_tasks::function(operation)]
125    pub async fn from_graph(graph: OperationVc<SingleModuleGraph>) -> Result<Vc<Self>> {
126        Ok(Self {
127            modules: graph
128                .connect()
129                .await?
130                .enumerate_nodes()
131                .flat_map(|(node_idx, module)| match module {
132                    SingleModuleGraphNode::Module(module) => Some((
133                        *module,
134                        GraphNodeIndex {
135                            graph_idx: 0,
136                            node_idx,
137                        },
138                    )),
139                    SingleModuleGraphNode::VisitedModule { .. } => None,
140                })
141                .collect(),
142            next_graph_idx: 1,
143        }
144        .cell())
145    }
146
147    #[turbo_tasks::function(operation)]
148    pub async fn with_incremented_index(this: OperationVc<Self>) -> Result<Vc<Self>> {
149        let this = this.connect().await?;
150        Ok(Self {
151            modules: this.modules.clone(),
152            next_graph_idx: this.next_graph_idx + 1,
153        }
154        .cell())
155    }
156
157    #[turbo_tasks::function(operation)]
158    pub async fn concatenate(
159        this: OperationVc<Self>,
160        graph: OperationVc<SingleModuleGraph>,
161    ) -> Result<Vc<Self>> {
162        let graph = graph.connect().await?;
163        let this = this.connect().await?;
164        let iter = this
165            .modules
166            .iter()
167            .map(|(module, idx)| (*module, *idx))
168            .chain(
169                graph
170                    .enumerate_nodes()
171                    .flat_map(|(node_idx, module)| match module {
172                        SingleModuleGraphNode::Module(module) => Some((
173                            *module,
174                            GraphNodeIndex {
175                                graph_idx: this.next_graph_idx,
176                                node_idx,
177                            },
178                        )),
179                        SingleModuleGraphNode::VisitedModule { .. } => None,
180                    }),
181            );
182
183        let mut map = FxIndexMap::with_capacity_and_hasher(
184            this.modules.len() + graph.number_of_modules,
185            Default::default(),
186        );
187        for (k, v) in iter {
188            map.entry(k).or_insert(v);
189        }
190        map.shrink_to_fit();
191
192        Ok(Self {
193            modules: map,
194            next_graph_idx: this.next_graph_idx + 1,
195        }
196        .cell())
197    }
198}
199
200#[turbo_tasks::value(shared, task_input)]
201#[derive(Debug, Clone, Hash, Default)]
202pub struct GraphEntries {
203    /// The bundled chunk groups (listing their entry modules)
204    chunk_groups: Vec<ChunkGroupEntry>,
205    /// Traced top-level modules, which are not referenced by chunk_groups but should still be
206    /// considered as part of the graph.
207    traced_modules: Vec<ResolvedVc<Box<dyn Module>>>,
208}
209
210#[turbo_tasks::value_impl]
211impl GraphEntries {
212    #[turbo_tasks::function]
213    pub fn empty() -> Vc<Self> {
214        Self::default().cell()
215    }
216}
217
218impl GraphEntries {
219    pub fn new(
220        chunk_groups: Vec<ChunkGroupEntry>,
221        traced_modules: Vec<ResolvedVc<Box<dyn Module>>>,
222    ) -> Self {
223        Self {
224            chunk_groups,
225            traced_modules,
226        }
227    }
228    pub fn from_chunk_groups(chunk_groups: Vec<ChunkGroupEntry>) -> Self {
229        Self {
230            chunk_groups,
231            traced_modules: vec![],
232        }
233    }
234
235    pub fn concatenate(entries: impl IntoIterator<Item = GraphEntries>) -> Self {
236        let (chunk_groups, traced_modules): (Vec<_>, Vec<_>) = entries
237            .into_iter()
238            .map(|e| (e.chunk_groups, e.traced_modules))
239            .unzip();
240        Self {
241            chunk_groups: chunk_groups.into_iter().flatten().collect(),
242            traced_modules: traced_modules.into_iter().flatten().collect(),
243        }
244    }
245
246    /// Returns both chunk group modules and traced modules.
247    pub fn all_modules(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + '_ {
248        self.chunk_groups
249            .iter()
250            .flat_map(|e| e.entries())
251            .chain(self.traced_modules.iter().cloned())
252    }
253
254    /// Like all_modules, but with a boolean whether the module came from `traced_modules`
255    pub fn all_modules_with_is_traced(
256        &self,
257    ) -> impl Iterator<Item = (ResolvedVc<Box<dyn Module>>, bool)> + '_ {
258        self.chunk_groups
259            .iter()
260            .flat_map(|e| e.entries().map(|m| (m, false)))
261            .chain(self.traced_modules.iter().cloned().map(|m| (m, true)))
262    }
263
264    /// Returns only the bundled modules, not the traced modules.
265    pub fn chunk_group_modules(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + '_ {
266        self.chunk_groups.iter().flat_map(|e| e.entries())
267    }
268}
269
270#[turbo_tasks::value(cell = "new", eq = "manual")]
271#[derive(Default)]
272pub struct SingleModuleGraph {
273    pub graph: TracedDiGraph<SingleModuleGraphNode, RefData>,
274
275    /// The number of modules in the graph (excluding VisitedModule nodes)
276    pub number_of_modules: usize,
277
278    // NodeIndex isn't necessarily stable (because of swap_remove), but we never remove nodes.
279    //
280    // HashMaps have nondeterministic order, but this map is only used for lookups (in
281    // `get_module`) and not iteration.
282    //
283    // This contains Vcs, but they are already contained in the graph.
284    #[turbo_tasks(unsafe_ignore)]
285    #[bincode(with_serde)]
286    modules: FxHashMap<ResolvedVc<Box<dyn Module>>, NodeIndex>,
287
288    entries: GraphEntries,
289
290    /// Derived from `entries` and `modules`. Both are immutable after graph construction, and node
291    /// indices are stable because graph nodes are never removed.
292    #[turbo_tasks(debug_ignore, unsafe_ignore)]
293    #[bincode(skip, default = "OnceLock::new")]
294    entry_nodes: OnceLock<FxHashSet<NodeIndex>>,
295}
296
297impl Clone for SingleModuleGraph {
298    fn clone(&self) -> Self {
299        Self {
300            graph: self.graph.clone(),
301            number_of_modules: self.number_of_modules,
302            modules: self.modules.clone(),
303            entries: self.entries.clone(),
304            // Never carry derived state into a clone that might be modified before being stored.
305            entry_nodes: OnceLock::new(),
306        }
307    }
308}
309
310#[derive(
311    Debug,
312    Clone,
313    Hash,
314    Serialize,
315    Deserialize,
316    Eq,
317    PartialEq,
318    ValueDebugFormat,
319    NonLocalValue,
320    Encode,
321    Decode,
322)]
323pub struct RefData {
324    pub chunking_type: ChunkingType,
325    pub binding_usage: BindingUsage,
326    pub reference: ResolvedVc<Box<dyn ModuleReference>>,
327}
328
329impl SingleModuleGraph {
330    /// Walks the graph starting from the given entries and collects all reachable nodes, skipping
331    /// nodes listed in `visited_modules`
332    /// The resulting graph's outgoing edges are in reverse order.
333    async fn new_inner(
334        entries: &GraphEntries,
335        visited_modules: &FxIndexMap<ResolvedVc<Box<dyn Module>>, GraphNodeIndex>,
336        include_traced: bool,
337        include_binding_usage: bool,
338    ) -> Result<Vc<Self>> {
339        let emit_spans = tracing::enabled!(Level::INFO);
340        let root_nodes = entries
341            .all_modules_with_is_traced()
342            .map(|(e, is_traced)| {
343                SingleModuleGraphBuilderNode::new_module(emit_spans, e, is_traced)
344            })
345            .try_join()
346            .await?;
347
348        let children_nodes_iter = AdjacencyMap::new()
349            .visit(
350                root_nodes,
351                SingleModuleGraphBuilder {
352                    visited_modules,
353                    emit_spans,
354                    include_traced,
355                    include_binding_usage,
356                },
357            )
358            .await
359            .completed()?;
360        let node_count = children_nodes_iter.len();
361
362        let mut graph: DiGraph<SingleModuleGraphNode, RefData> = DiGraph::with_capacity(
363            node_count,
364            // From real world measurements each module has about 3-4 children
365            // If it has more this would cause an additional allocation, but that's fine
366            node_count * 4,
367        );
368
369        let mut number_of_modules = 0;
370        let mut modules: FxHashMap<ResolvedVc<Box<dyn Module>>, NodeIndex> =
371            FxHashMap::with_capacity_and_hasher(node_count, Default::default());
372        {
373            let _span = tracing::info_span!("build module graph").entered();
374            for (parent, current) in children_nodes_iter.into_breadth_first_edges() {
375                let (module, graph_node, count) = match current {
376                    SingleModuleGraphBuilderNode::Module {
377                        module,
378                        is_traced: _,
379                        ident: _,
380                    } => (module, SingleModuleGraphNode::Module(module), 1),
381                    SingleModuleGraphBuilderNode::VisitedModule { module, idx } => (
382                        module,
383                        SingleModuleGraphNode::VisitedModule { idx, module },
384                        0,
385                    ),
386                };
387
388                // Find the current node, if it was already added
389                let current_idx = if let Some(current_idx) = modules.get(&module) {
390                    *current_idx
391                } else {
392                    let idx = graph.add_node(graph_node);
393                    number_of_modules += count;
394                    modules.insert(module, idx);
395                    idx
396                };
397                // Add the edge
398                if let Some((SingleModuleGraphBuilderNode::Module { module, .. }, ref_data)) =
399                    parent
400                {
401                    let parent_idx = *modules.get(&module).unwrap();
402                    graph.add_edge(parent_idx, current_idx, ref_data);
403                }
404            }
405        }
406
407        graph.shrink_to_fit();
408
409        #[cfg(debug_assertions)]
410        {
411            use std::sync::LazyLock;
412            static CHECK_FOR_DUPLICATE_MODULES: LazyLock<bool> = LazyLock::new(|| {
413                match std::env::var_os("TURBOPACK_TEMP_DISABLE_DUPLICATE_MODULES_CHECK") {
414                    Some(v) => v != "1" && v != "true",
415                    None => true,
416                }
417            });
418            if *CHECK_FOR_DUPLICATE_MODULES {
419                let mut duplicates = FxHashSet::default();
420                let mut set = FxHashSet::default();
421                for &module in modules.keys() {
422                    let ident = module.ident().to_string().await?;
423                    if !set.insert(ident.clone()) {
424                        duplicates.insert(ident);
425                    }
426                }
427                if !duplicates.is_empty() {
428                    use turbo_tasks::TryFlatJoinIterExt;
429
430                    let duplicates_clone = duplicates.clone();
431                    let duplicate_modules = modules
432                        .iter()
433                        .map(async |(&m, &idx)| {
434                            let id = m.ident().to_string().await?;
435                            if duplicates_clone.contains(&id) {
436                                // 3 is arbitrary but it is enough to reveal a little bit
437                                // of detail.
438                                let debug = m.value_debug_format(3).try_to_string().await?;
439
440                                // Collect reverse dependencies (parents) to help
441                                // diagnose how this module entered the graph.
442                                let parent_modules: Vec<_> = graph
443                                    .edges_directed(idx, petgraph::Direction::Incoming)
444                                    .filter_map(|edge| match graph.node_weight(edge.source()) {
445                                        Some(SingleModuleGraphNode::Module(m)) => Some(*m),
446                                        Some(SingleModuleGraphNode::VisitedModule {
447                                            module,
448                                            ..
449                                        }) => Some(*module),
450                                        None => None,
451                                    })
452                                    .collect();
453                                let parents: Vec<String> = parent_modules
454                                    .iter()
455                                    .map(async |p| {
456                                        let ident = p.ident().to_string().await?;
457                                        Ok((*ident).to_string())
458                                    })
459                                    .try_join()
460                                    .await?;
461
462                                Ok(Some((id, debug, parents)))
463                            } else {
464                                Ok(None)
465                            }
466                        })
467                        .try_flat_join()
468                        .await?;
469                    // group by ident
470                    let mut map: FxHashMap<_, Vec<(String, Vec<String>)>> = FxHashMap::default();
471                    for (key, debug, parents) in duplicate_modules {
472                        map.entry(key).or_default().push((debug, parents));
473                    }
474                    let result = map
475                        .into_iter()
476                        .map(|(ident, modules)| {
477                            let modules = modules
478                                .into_iter()
479                                .map(|(debug, parents)| {
480                                    format!("Module: {debug}, Parents: {parents:?}")
481                                })
482                                .collect::<Vec<_>>()
483                                .join("\n");
484                            format!("Ident: {ident}\n{modules}")
485                        })
486                        .collect::<Vec<_>>()
487                        .join("\n\n");
488                    bail!("Duplicate module idents in graph: {result}");
489                }
490            }
491        }
492
493        let graph = SingleModuleGraph {
494            graph: TracedDiGraph::new(graph),
495            number_of_modules,
496            modules,
497            entries: entries.clone(),
498            entry_nodes: OnceLock::new(),
499        }
500        .cell();
501
502        turbo_tasks::emit(ResolvedVc::upcast::<Box<dyn ImportTracer>>(
503            ModuleGraphImportTracer::new(graph).to_resolved().await?,
504        ));
505        Ok(graph)
506    }
507
508    /// WARNING: using this is discouraged, as it doesn't filter out unused or traced references.
509    /// Use iter_reachable_modules or one of the .traverse_* functions instead.
510    pub fn iter_nodes(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + '_ {
511        self.graph.node_weights().filter_map(|n| match n {
512            SingleModuleGraphNode::Module(node) => Some(*node),
513            SingleModuleGraphNode::VisitedModule { .. } => None,
514        })
515    }
516
517    fn entry_nodes(&self) -> &FxHashSet<NodeIndex> {
518        self.entry_nodes.get_or_init(|| {
519            self.entries
520                .all_modules()
521                .filter_map(|module| self.modules.get(&module).copied())
522                .collect()
523        })
524    }
525
526    /// Returns true if the given module is in this graph and is an entry module.
527    ///
528    /// Entry modules are tracked explicitly because an entry can have incoming edges when it is
529    /// part of a module cycle.
530    pub fn has_entry_module(&self, module: ResolvedVc<Box<dyn Module>>) -> bool {
531        self.modules
532            .get(&module)
533            .is_some_and(|index| self.entry_nodes().contains(index))
534    }
535
536    /// Iterate over graph entry points
537    pub fn chunk_group_modules(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + '_ {
538        self.entries.chunk_group_modules()
539    }
540
541    /// WARNING: using this is discouraged, as it doesn't filter out unused or traced references.
542    /// Use iter_reachable_modules or one of the .traverse_* functions instead.
543    pub fn enumerate_nodes(
544        &self,
545    ) -> impl Iterator<Item = (NodeIndex, &'_ SingleModuleGraphNode)> + '_ {
546        self.graph.node_references()
547    }
548
549    fn traverse_cycles<'l>(
550        &'l self,
551        edge_filter: impl Fn(&'l RefData) -> bool,
552        mut visit_cycle: impl FnMut(&[&'l ResolvedVc<Box<dyn Module>>]) -> Result<()>,
553        graph_idx: u32,
554        unused_references_edges: &'l Option<ReadRef<UnusedReferenceEdges>>,
555    ) -> Result<()> {
556        // See https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm, but
557        // implemented iteratively instead of recursively.
558        //
559        // Compared to the standard Tarjan's, this also treated self-references (via
560        // `has_self_loop`) as SCCs.
561
562        #[derive(Clone)]
563        struct NodeState {
564            index: u32,
565            lowlink: u32,
566            on_stack: bool,
567            has_self_loop: bool,
568        }
569        enum VisitStep {
570            UnvisitedNode(NodeIndex),
571            EdgeAfterVisit { parent: NodeIndex, child: NodeIndex },
572            AfterVisit(NodeIndex),
573        }
574        let mut node_states = vec![None; self.graph.node_bound()];
575        let mut stack = Vec::new();
576        let mut visit_stack = Vec::new();
577        let mut index = 0;
578        let mut scc = Vec::new();
579        for initial_index in self.graph.node_indices() {
580            // Skip over already visited nodes
581            if node_states[initial_index.index()].is_some() {
582                continue;
583            }
584            visit_stack.push(VisitStep::UnvisitedNode(initial_index));
585            while let Some(step) = visit_stack.pop() {
586                match step {
587                    VisitStep::UnvisitedNode(node) => {
588                        node_states[node.index()] = Some(NodeState {
589                            index,
590                            lowlink: index,
591                            on_stack: true,
592                            has_self_loop: false,
593                        });
594                        index += 1;
595                        stack.push(node);
596                        visit_stack.push(VisitStep::AfterVisit(node));
597                        let mut neighbors = self.graph.neighbors(node).detach();
598                        while let Some((edge, succ)) = neighbors.next(&self.graph) {
599                            if unused_references_edges.as_ref().is_some_and(|edges| {
600                                edges.contains_key(&GraphEdgeIndex::new(graph_idx, edge))
601                            }) {
602                                continue;
603                            }
604
605                            let edge_weight = self.graph.edge_weight(edge).unwrap();
606                            if !edge_filter(edge_weight) {
607                                continue;
608                            }
609                            let node_state = &node_states[succ.index()];
610                            if let Some(node_state) = node_state {
611                                if node_state.on_stack {
612                                    let index = node_state.index;
613                                    let parent_state = node_states[node.index()].as_mut().unwrap();
614                                    parent_state.lowlink = parent_state.lowlink.min(index);
615                                    if succ == node {
616                                        parent_state.has_self_loop = true;
617                                    }
618                                }
619                            } else {
620                                visit_stack.push(VisitStep::EdgeAfterVisit {
621                                    parent: node,
622                                    child: succ,
623                                });
624                                visit_stack.push(VisitStep::UnvisitedNode(succ));
625                            }
626                        }
627                    }
628                    VisitStep::EdgeAfterVisit { parent, child } => {
629                        let child_state = node_states[child.index()].as_ref().unwrap();
630                        let lowlink = child_state.lowlink;
631
632                        let parent_state = node_states[parent.index()].as_mut().unwrap();
633                        parent_state.lowlink = parent_state.lowlink.min(lowlink);
634                    }
635                    VisitStep::AfterVisit(node) => {
636                        let node_state = node_states[node.index()].as_ref().unwrap();
637                        let node_has_self_loop = node_state.has_self_loop;
638                        if node_state.lowlink == node_state.index {
639                            loop {
640                                let poppped = stack.pop().unwrap();
641                                let popped_state = node_states[poppped.index()].as_mut().unwrap();
642                                popped_state.on_stack = false;
643                                if let SingleModuleGraphNode::Module(module) =
644                                    self.graph.node_weight(poppped).unwrap()
645                                {
646                                    scc.push(module);
647                                }
648                                if poppped == node {
649                                    break;
650                                }
651                            }
652                            if scc.len() > 1 || node_has_self_loop {
653                                visit_cycle(&scc)?;
654                            }
655                            scc.clear();
656                        }
657                    }
658                }
659            }
660        }
661        Ok(())
662    }
663}
664
665#[turbo_tasks::value]
666struct ModuleGraphImportTracer {
667    graph: ResolvedVc<SingleModuleGraph>,
668}
669
670#[turbo_tasks::value(shared)]
671struct PathToModulesMap {
672    map: FxHashMap<FileSystemPath, Vec<ResolvedVc<Box<dyn Module>>>>,
673}
674
675#[turbo_tasks::value_impl]
676impl ModuleGraphImportTracer {
677    #[turbo_tasks::function]
678    fn new(graph: ResolvedVc<SingleModuleGraph>) -> Vc<Self> {
679        Self::cell(Self { graph })
680    }
681
682    // Compute this mapping on demand since it might not always be needed.
683    #[turbo_tasks::function]
684    async fn path_to_modules(&self) -> Result<Vc<PathToModulesMap>> {
685        let path_and_modules = self
686            .graph
687            .await?
688            .modules
689            .iter()
690            .map(async |(&module, _)| Ok((module.ident().await?.path.clone(), module)))
691            .try_join()
692            .await?;
693        let mut map: FxHashMap<FileSystemPath, Vec<ResolvedVc<Box<dyn Module>>>> =
694            FxHashMap::default();
695        for (path, module) in path_and_modules {
696            map.entry(path).or_default().push(module)
697        }
698        Ok(PathToModulesMap::cell(PathToModulesMap { map }))
699    }
700}
701
702#[turbo_tasks::value_impl]
703impl ImportTracer for ModuleGraphImportTracer {
704    #[turbo_tasks::function]
705    async fn get_traces(self: Vc<Self>, path: FileSystemPath) -> Result<Vc<ImportTraces>> {
706        let path_to_modules = self.path_to_modules().await?;
707        let Some(modules) = path_to_modules.map.get(&path) else {
708            return Ok(Vc::default()); // This isn't unusual, the file just might not be in this
709            // graph.
710        };
711        debug_assert!(!modules.is_empty(), "modules should not be an empty vec");
712        let graph = &*self.await?.graph.await?;
713
714        let reversed_graph = Reversed(&graph.graph.0);
715        // A graph entry may have incoming edges when it participates in a cycle, so roots cannot
716        // be inferred from graph topology alone.
717        let root_nodes = graph.entry_nodes();
718        return Ok(ImportTraces::cell(ImportTraces(
719            modules
720                .iter()
721                .map(async |m| {
722                    let Some(&module_idx) = graph.modules.get(m) else {
723                        // The only way this could really happen is if `path_to_modules` is computed
724                        // from a different graph than graph`.  Just error out.
725                        bail!("inconsistent read?")
726                    };
727                    // Compute the path from this index to an explicit root of the graph.
728                    let path = match petgraph::algo::astar(
729                        &reversed_graph,
730                        module_idx,
731                        |n| root_nodes.contains(&n),
732                        // Edge weights
733                        |e| match e.weight().chunking_type {
734                            // Prefer following normal imports/requires when we can
735                            ChunkingType::Parallel { .. } => 0,
736                            _ => 1,
737                        },
738                        // `astar` can be accelerated with a distance estimation heuristic, as long
739                        // as our estimate is never > the actual distance.
740                        // However we don't have a mechanism, so just
741                        // estimate 0 which essentially makes this behave like
742                        // dijktra's shortest path algorithm.  `petgraph` has an implementation of
743                        // dijkstra's but it doesn't report  paths, just distances.
744                        // NOTE: dijkstra's with integer weights can be accelerated with incredibly
745                        // efficient priority queue structures (basically with only 0 and 1 as
746                        // weights you can use a `VecDeque`!).  However,
747                        // this is unlikely to be a performance concern.
748                        // Furthermore, if computing paths _does_ become a performance concern, the
749                        // solution would be a hand written implementation of dijkstras so we can
750                        // hoist redundant work out of this loop.
751                        |_| 0,
752                    ) {
753                        Some((_, path)) => path,
754                        None => {
755                            let module = graph
756                                .graph
757                                .node_weight(module_idx)
758                                .expect("module index must be present in the graph")
759                                .module();
760                            AnalyzeIssue::new(
761                                IssueSeverity::Bug,
762                                module.ident(),
763                                rcstr!("Module graph is missing an entry point"),
764                                rcstr!(
765                                    "The module cannot reach any of the explicit entry points in \
766                                     its module graph."
767                                ),
768                                None,
769                                None,
770                            )
771                            .to_resolved()
772                            .await?
773                            .emit();
774                            vec![module_idx]
775                        }
776                    };
777
778                    // Represent the path as a sequence of AssetIdents
779                    // TODO: consider hinting at various transitions (e.g. was this an
780                    // import/require/dynamic-import?)
781                    let path = path
782                        .into_iter()
783                        .map(|n| {
784                            graph
785                                .graph
786                                .node_weight(n)
787                                .unwrap() // This is safe since `astar`` only returns indices from the graph
788                                .module()
789                                .ident()
790                        })
791                        .try_join()
792                        .await?;
793                    Ok(path)
794                })
795                .try_join()
796                .await?,
797        )));
798    }
799}
800
801/// The ReadRef version of ModuleGraphBase. This is better for eventual consistency, as the graphs
802/// aren't awaited multiple times within the same task.
803#[turbo_tasks::value(shared, serialization = "skip", eq = "manual", cell = "new")]
804pub struct ModuleGraph {
805    input_graphs: Vec<OperationVc<SingleModuleGraph>>,
806    input_binding_usage: Option<OperationVc<BindingUsageInfo>>,
807
808    snapshot: ModuleGraphSnapshot,
809}
810
811#[turbo_tasks::value_impl]
812impl ModuleGraph {
813    /// Analyze the module graph and potentially remove unused references (by determining the used
814    /// exports and removing unused imports).
815    #[turbo_tasks::function(operation)]
816    pub async fn from_graphs(
817        graphs: Vec<OperationVc<SingleModuleGraph>>,
818        binding_usage: Option<OperationVc<BindingUsageInfo>>,
819    ) -> Result<Vc<Self>> {
820        let graph = Self::from_graphs_inner(graphs, binding_usage)
821            .read_strongly_consistent()
822            .await?;
823
824        Ok(ReadRef::cell(graph))
825    }
826
827    #[turbo_tasks::function(operation, root)]
828    async fn from_graphs_inner(
829        graphs: Vec<OperationVc<SingleModuleGraph>>,
830        binding_usage: Option<OperationVc<BindingUsageInfo>>,
831    ) -> Result<Vc<ModuleGraph>> {
832        Ok(ModuleGraph {
833            input_graphs: graphs.clone(),
834            input_binding_usage: binding_usage,
835            snapshot: ModuleGraphSnapshot {
836                graphs: graphs.iter().map(|g| g.connect()).try_join().await?,
837                skip_visited_module_children: false,
838                graph_idx_override: None,
839                unused_references_edges: if let Some(binding_usage) = binding_usage {
840                    Some(binding_usage.connect().unused_references_edges().await?)
841                } else {
842                    None
843                },
844            },
845        }
846        .cell())
847    }
848
849    #[turbo_tasks::function]
850    pub async fn collected_modules(self: Vc<Self>) -> Result<Vc<CollectedModules>> {
851        collect_graph(self).await
852    }
853
854    #[turbo_tasks::function]
855    pub async fn chunk_group_info(self: Vc<Self>) -> Result<Vc<ChunkGroupInfo>> {
856        compute_chunk_group_info(&*self.await?).await
857    }
858
859    #[turbo_tasks::function]
860    pub async fn merged_modules(self: Vc<Self>) -> Result<Vc<MergedModuleInfo>> {
861        compute_merged_modules(self).await
862    }
863
864    #[turbo_tasks::function]
865    pub async fn module_batches(
866        self: Vc<Self>,
867        config: Vc<BatchingConfig>,
868    ) -> Result<Vc<ModuleBatchesGraph>> {
869        compute_module_batches(self, &*config.await?).await
870    }
871
872    #[turbo_tasks::function]
873    pub async fn style_groups(
874        self: Vc<Self>,
875        chunking_context: Vc<Box<dyn ChunkingContext>>,
876        config: StyleGroupsConfig,
877    ) -> Result<Vc<StyleGroups>> {
878        match &config.algorithm {
879            StyleGroupsAlgorithm::Default => {
880                compute_style_groups(self, chunking_context, &config).await
881            }
882            StyleGroupsAlgorithm::Graph {
883                weight_distribution,
884                request_cost,
885            } => {
886                compute_style_groups_graph(
887                    self,
888                    chunking_context,
889                    request_cost.get(),
890                    weight_distribution.get(),
891                    config.max_chunk_size as u64,
892                )
893                .await
894            }
895        }
896    }
897
898    #[turbo_tasks::function(root)]
899    pub async fn async_module_info(self: Vc<Self>) -> Result<Vc<AsyncModulesInfo>> {
900        // `compute_async_module_info` calls `module.is_self_async()`, so we need to again ignore
901        // all issues such that they aren't emitted multiple times.
902        async move {
903            let result_op = compute_async_module_info(self.to_resolved().await?);
904            let result_vc = result_op.resolve().strongly_consistent().await?;
905            result_op.drop_collectibles::<Box<dyn Issue>>();
906            anyhow::Ok(*result_vc)
907        }
908        .instrument(tracing::info_span!("compute async module info"))
909        .await
910    }
911
912    #[turbo_tasks::function]
913    pub async fn referenced_async_modules(
914        self: Vc<Self>,
915        module: ResolvedVc<Box<dyn Module>>,
916    ) -> Result<Vc<AsyncModuleInfo>> {
917        let graph_ref = self.await?;
918        let async_module_info = self.async_module_info();
919
920        let entry = graph_ref.get_entry(module)?;
921        let referenced_modules = graph_ref
922            .iter_graphs_neighbors_rev(entry, Direction::Outgoing, false, false)
923            .filter(|(edge_idx, _)| {
924                let ty = graph_ref.get_edge(*edge_idx).unwrap();
925                ty.chunking_type.is_inherit_async()
926            })
927            .map(|(_, child_idx)| anyhow::Ok(graph_ref.get_node(child_idx)?.module()))
928            .collect::<Result<Vec<_>>>()?
929            .into_iter()
930            .rev()
931            .map(async |m| Ok(async_module_info.is_async(m).await?.then_some(*m)))
932            .try_flat_join()
933            .await?;
934
935        Ok(AsyncModuleInfo::new(referenced_modules))
936    }
937
938    /// Returns the underlying graphs as a list, to be used for individual graph traversals.
939    #[turbo_tasks::function]
940    pub fn iter_graphs(&self) -> Vc<ModuleGraphLayers> {
941        Vc::cell(
942            self.input_graphs
943                .iter()
944                .enumerate()
945                .map(|(graph_idx, graph)| {
946                    ModuleGraphLayer::new(*graph, graph_idx as u32, self.input_binding_usage)
947                })
948                .collect(),
949        )
950    }
951}
952
953impl Deref for ModuleGraph {
954    type Target = ModuleGraphSnapshot;
955
956    fn deref(&self) -> &Self::Target {
957        &self.snapshot
958    }
959}
960
961#[turbo_tasks::value(shared, serialization = "skip", eq = "manual", cell = "new")]
962pub struct ModuleGraphLayer {
963    snapshot: ModuleGraphSnapshot,
964}
965
966#[turbo_tasks::value_impl]
967impl ModuleGraphLayer {
968    #[turbo_tasks::function(operation, root)]
969    async fn new(
970        graph: OperationVc<SingleModuleGraph>,
971        graph_idx: u32,
972        binding_usage: Option<OperationVc<BindingUsageInfo>>,
973    ) -> Result<Vc<Self>> {
974        Ok(Self {
975            snapshot: ModuleGraphSnapshot {
976                graphs: vec![graph.connect().await?],
977                skip_visited_module_children: true,
978                graph_idx_override: Some(graph_idx),
979                unused_references_edges: if let Some(binding_usage) = binding_usage {
980                    Some(binding_usage.connect().unused_references_edges().await?)
981                } else {
982                    None
983                },
984            },
985        }
986        .cell())
987    }
988}
989
990impl Deref for ModuleGraphLayer {
991    type Target = ModuleGraphSnapshot;
992
993    fn deref(&self) -> &Self::Target {
994        &self.snapshot
995    }
996}
997
998#[turbo_tasks::value(transparent)]
999pub struct ModuleGraphLayers(Vec<OperationVc<ModuleGraphLayer>>);
1000
1001/// This struct provides traversal functionality for the module graph.
1002///
1003/// Some edges might be ignored during traversal: unused references listed in
1004/// unused_references_edges and references with ChunkingType::Traced are skipped by default (can be
1005/// overridden in some functions via the include_unused and include_traced parameters).
1006///
1007/// The API across the functions is pretty consistent, apart from:
1008/// - traverse_edges_fixed_point_with_priority additionally provides the GraphEdgeIndex
1009/// - traverse_edges_dfs is the only function with include_traced and include_unused
1010#[derive(ValueDebugFormat, NonLocalValue)]
1011pub struct ModuleGraphSnapshot {
1012    // TODO make this non-public
1013    pub graphs: Vec<ReadRef<SingleModuleGraph>>,
1014    /// Whether to simply ignore SingleModuleGraphNode::VisitedModule during traversals. For single
1015    /// module graph usecases, this is what you want. For the whole graph, there should be an
1016    /// error.
1017    skip_visited_module_children: bool,
1018
1019    graph_idx_override: Option<u32>,
1020
1021    /// The edges of unused references (from [`BindingUsageInfo::unused_references_edges`]), which
1022    /// are skipped during traversals.
1023    unused_references_edges: Option<ReadRef<UnusedReferenceEdges>>,
1024}
1025
1026impl ModuleGraphSnapshot {
1027    fn get_entry(&self, entry: ResolvedVc<Box<dyn Module>>) -> Result<GraphNodeIndex> {
1028        if self.graph_idx_override.is_some() {
1029            debug_assert_eq!(self.graphs.len(), 1,);
1030        }
1031
1032        let Some(idx) = self
1033            .graphs
1034            .iter()
1035            .enumerate()
1036            .find_map(|(graph_idx, graph)| {
1037                graph.modules.get(&entry).map(|node_idx| GraphNodeIndex {
1038                    graph_idx: self.graph_idx_override.unwrap_or(graph_idx as u32),
1039                    node_idx: *node_idx,
1040                })
1041            })
1042        else {
1043            bail!("Couldn't find entry module {entry:?} in module graph");
1044        };
1045        Ok(idx)
1046    }
1047
1048    /// The entry modules of all chunk groups of all graphs.
1049    pub fn all_chunk_group_entries(&self) -> impl Iterator<Item = &ChunkGroupEntry> + '_ {
1050        self.graphs
1051            .iter()
1052            .flat_map(|g| g.entries.chunk_groups.iter())
1053    }
1054
1055    /// The entry modules of all chunk groups of all graphs.
1056    pub fn all_chunk_group_entry_modules(
1057        &self,
1058    ) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + '_ {
1059        self.graphs
1060            .iter()
1061            .flat_map(|g| g.entries.chunk_group_modules())
1062    }
1063
1064    /// The entry modules of all chunk groups of all graphs. Includes traced entry modules
1065    pub fn all_entry_modules(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + '_ {
1066        self.graphs.iter().flat_map(|g| g.entries.all_modules())
1067    }
1068
1069    fn get_graph(&self, graph_idx: u32) -> &ReadRef<SingleModuleGraph> {
1070        if self.graph_idx_override.is_some() {
1071            self.graphs.first().unwrap()
1072        } else {
1073            &self.graphs[graph_idx as usize]
1074        }
1075    }
1076
1077    fn get_node(&self, node: GraphNodeIndex) -> Result<&SingleModuleGraphNode> {
1078        let graph = self.get_graph(node.graph_idx);
1079        graph
1080            .graph
1081            .node_weight(node.node_idx)
1082            .context("Expected graph node")
1083    }
1084
1085    fn get_edge(&self, edge: GraphEdgeIndex) -> Result<&RefData> {
1086        let graph = self.get_graph(edge.graph_idx);
1087        graph
1088            .graph
1089            .edge_weight(edge.edge_idx)
1090            .context("Expected graph node")
1091    }
1092
1093    fn should_visit_node(&self, node: &SingleModuleGraphNode, direction: Direction) -> bool {
1094        if self.skip_visited_module_children && direction == Direction::Outgoing {
1095            !matches!(node, SingleModuleGraphNode::VisitedModule { .. })
1096        } else {
1097            true
1098        }
1099    }
1100
1101    /// WARNING: using this is discouraged, as it doesn't filter out unused or traced references.
1102    /// Use iter_reachable_modules or one of the .traverse_* functions instead.
1103    pub fn enumerate_nodes(
1104        &self,
1105    ) -> impl Iterator<Item = (NodeIndex, &'_ SingleModuleGraphNode)> + '_ {
1106        self.graphs.iter().flat_map(|g| g.enumerate_nodes())
1107    }
1108
1109    /// WARNING: using this is discouraged, as it doesn't filter out unused or traced references.
1110    /// Use iter_reachable_modules or one of the .traverse_* functions instead.
1111    pub fn iter_nodes(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + '_ {
1112        self.graphs.iter().flat_map(|g| g.iter_nodes())
1113    }
1114
1115    /// Iterate the edges of a node REVERSED!
1116    fn iter_graphs_neighbors_rev<'a>(
1117        &'a self,
1118        node: GraphNodeIndex,
1119        direction: Direction,
1120        include_traced: bool,
1121        include_unused: bool,
1122    ) -> impl Iterator<Item = (GraphEdgeIndex, GraphNodeIndex)> + 'a {
1123        let graph = &*self.get_graph(node.graph_idx).graph;
1124
1125        if cfg!(debug_assertions) && direction == Direction::Outgoing {
1126            let node_weight = graph.node_weight(node.node_idx).unwrap();
1127            if let SingleModuleGraphNode::VisitedModule { .. } = node_weight {
1128                panic!("iter_graphs_neighbors_rev called on VisitedModule node");
1129            }
1130        }
1131
1132        let mut walker = graph.neighbors_directed(node.node_idx, direction).detach();
1133        std::iter::from_fn(move || {
1134            while let Some((edge_idx, succ_idx)) = walker.next(graph) {
1135                let edge_idx = GraphEdgeIndex::new(node.graph_idx, edge_idx);
1136                if !include_unused
1137                    && self
1138                        .unused_references_edges
1139                        .as_ref()
1140                        .is_some_and(|edges| edges.contains_key(&edge_idx))
1141                {
1142                    // Don't just return None here, that would end the iterator
1143                    continue;
1144                }
1145
1146                if !include_traced && self.get_edge(edge_idx).unwrap().chunking_type.is_traced() {
1147                    continue;
1148                }
1149
1150                return Some((edge_idx, GraphNodeIndex::new(node.graph_idx, succ_idx)));
1151            }
1152            None
1153        })
1154    }
1155
1156    /// Returns a map of all modules in the graphs to their identifiers.
1157    /// This is primarily useful for debugging.
1158    pub async fn get_ids(&self) -> Result<FxHashMap<ResolvedVc<Box<dyn Module>>, ReadRef<RcStr>>> {
1159        self.iter_nodes()
1160            .map(async |n| Ok((n, n.ident().to_string().await?)))
1161            .join()
1162            .await
1163            .into_iter()
1164            .collect()
1165    }
1166
1167    /// Traverses all reachable nodes exactly once and calls the visitor.
1168    ///
1169    /// * `entries` - The entry modules to start the traversal from
1170    /// * `state` mutable state to be shared across the visitors
1171    /// * `visit_preorder` - Called before visiting the children of a node.
1172    ///    - Receives the module and the `state`
1173    ///    - Can return [GraphTraversalAction]s to control the traversal
1174    /// * `visit_postorder` - Called after visiting children of a node.
1175    pub fn traverse_nodes_dfs<S>(
1176        &self,
1177        entries: impl IntoIterator<Item = ResolvedVc<Box<dyn Module>>>,
1178        state: &mut S,
1179        visit_preorder: impl Fn(ResolvedVc<Box<dyn Module>>, &mut S) -> Result<GraphTraversalAction>,
1180        mut visit_postorder: impl FnMut(ResolvedVc<Box<dyn Module>>, &mut S) -> Result<()>,
1181    ) -> Result<()> {
1182        let entries = entries.into_iter().collect::<Vec<_>>();
1183
1184        enum Pass {
1185            Visit,
1186            ExpandAndVisit,
1187        }
1188        let mut stack: Vec<(Pass, GraphNodeIndex)> = Vec::with_capacity(entries.len());
1189        for entry in entries.into_iter().rev() {
1190            stack.push((Pass::ExpandAndVisit, self.get_entry(entry)?));
1191        }
1192        let mut expanded = FxHashSet::default();
1193        while let Some((pass, current)) = stack.pop() {
1194            let current_node = self.get_node(current)?;
1195            match pass {
1196                Pass::Visit => {
1197                    visit_postorder(current_node.module(), state)?;
1198                }
1199                Pass::ExpandAndVisit => {
1200                    if !expanded.insert(current) {
1201                        continue;
1202                    }
1203                    let action = visit_preorder(current_node.module(), state)?;
1204                    if action == GraphTraversalAction::Exclude {
1205                        continue;
1206                    }
1207                    stack.push((Pass::Visit, current));
1208                    if action == GraphTraversalAction::Continue
1209                        && self.should_visit_node(current_node, Direction::Outgoing)
1210                    {
1211                        let current = current_node
1212                            .target_idx(Direction::Outgoing)
1213                            .unwrap_or(current);
1214                        stack.extend(
1215                            self.iter_graphs_neighbors_rev(
1216                                current,
1217                                Direction::Outgoing,
1218                                false,
1219                                false,
1220                            )
1221                            .map(|(_, child)| (Pass::ExpandAndVisit, child)),
1222                        );
1223                    }
1224                }
1225            }
1226        }
1227
1228        Ok(())
1229    }
1230
1231    /// Traverses all reachable edges exactly once and calls the visitor with the edge source and
1232    /// target.
1233    ///
1234    /// This means that target nodes can be revisited (once per incoming edge).
1235    ///
1236    /// * `entry` - The entry module to start the traversal from
1237    /// * `visitor` - Called before visiting the children of a node.
1238    ///    - Receives (originating &SingleModuleGraphNode, edge &ChunkingType), target
1239    ///      &SingleModuleGraphNode, state &S
1240    ///    - Can return [GraphTraversalAction]s to control the traversal
1241    pub fn traverse_edges_bfs(
1242        &self,
1243        entries: impl IntoIterator<Item = ResolvedVc<Box<dyn Module>>>,
1244        mut visitor: impl FnMut(
1245            Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData)>,
1246            ResolvedVc<Box<dyn Module>>,
1247        ) -> Result<GraphTraversalAction>,
1248    ) -> Result<()> {
1249        let mut queue = VecDeque::from(
1250            entries
1251                .into_iter()
1252                .map(|e| self.get_entry(e))
1253                .collect::<Result<Vec<_>>>()?,
1254        );
1255        let mut visited = FxHashSet::default();
1256        for entry_node in &queue {
1257            visitor(None, self.get_node(*entry_node)?.module())?;
1258        }
1259        while let Some(node) = queue.pop_front() {
1260            if visited.insert(node) {
1261                let node_weight = self.get_node(node)?;
1262                for (edge, succ) in
1263                    self.iter_graphs_neighbors_rev(node, Direction::Outgoing, false, false)
1264                {
1265                    let succ_weight = self.get_node(succ)?;
1266                    let action = visitor(
1267                        Some((node_weight.module(), self.get_edge(edge)?)),
1268                        succ_weight.module(),
1269                    )?;
1270                    if !self.should_visit_node(succ_weight, Direction::Outgoing) {
1271                        continue;
1272                    }
1273                    let succ = succ_weight.target_idx(Direction::Outgoing).unwrap_or(succ);
1274                    if !visited.contains(&succ) && action == GraphTraversalAction::Continue {
1275                        queue.push_back(succ);
1276                    }
1277                }
1278            }
1279        }
1280
1281        Ok(())
1282    }
1283
1284    /// Traverses all edges exactly once (in an unspecified order) and calls the visitor with the
1285    /// edge source and target.
1286    ///
1287    /// This means that target nodes can be revisited (once per incoming edge).
1288    ///
1289    /// * `visitor` - Called before visiting the children of a node.
1290    ///    - Receives (originating &SingleModuleGraphNode, edge &ChunkingType), target
1291    ///      &SingleModuleGraphNode
1292    pub fn traverse_edges_unordered(
1293        &self,
1294        mut visitor: impl FnMut(
1295            Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData)>,
1296            ResolvedVc<Box<dyn Module>>,
1297        ) -> Result<()>,
1298    ) -> Result<()> {
1299        // Despite the name we need to do a DFS to respect 'reachability' if an edge was trimmed we
1300        // should not follow it, and this is a reasonable way to do that.
1301        self.traverse_edges_dfs(
1302            self.all_chunk_group_entry_modules(),
1303            &mut (),
1304            |parent, target, _| {
1305                visitor(parent, target)?;
1306                Ok(GraphTraversalAction::Continue)
1307            },
1308            |_, _, _| Ok(()),
1309            false,
1310            false,
1311        )
1312    }
1313
1314    /// Traverses all reachable edges in dfs order. The preorder visitor can be used to
1315    /// forward state down the graph, and to skip subgraphs
1316    ///
1317    /// Use this to collect modules in evaluation order.
1318    ///
1319    /// Target nodes can be revisited (once per incoming edge) in the preorder_visitor, in the post
1320    /// order visitor they are visited exactly once with the first edge they were discovered with.
1321    /// Edges are traversed in normal order, so should correspond to reference order.
1322    ///
1323    /// * `entries` - The entry modules to start the traversal from
1324    /// * `state` - The state to be passed to the visitors
1325    /// * `visit_preorder` - Called before visiting the children of a node.
1326    ///    - Receives: (originating &SingleModuleGraphNode, edge &ChunkingType), target
1327    ///      &SingleModuleGraphNode, state &S
1328    ///    - Can return [GraphTraversalAction]s to control the traversal
1329    /// * `visit_postorder` - Called after visiting the children of a node. Return
1330    ///    - Receives: (originating &SingleModuleGraphNode, edge &ChunkingType), target
1331    ///      &SingleModuleGraphNode, state &S
1332    /// * `include_traced` - Include references with ChunkingType::Traced.
1333    /// * `include_unused` - Include references marked unused by binding usage.
1334    pub fn traverse_edges_dfs<S>(
1335        &self,
1336        entries: impl IntoIterator<Item = ResolvedVc<Box<dyn Module>>>,
1337        state: &mut S,
1338        visit_preorder: impl FnMut(
1339            Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData)>,
1340            ResolvedVc<Box<dyn Module>>,
1341            &mut S,
1342        ) -> Result<GraphTraversalAction>,
1343        visit_postorder: impl FnMut(
1344            Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData)>,
1345            ResolvedVc<Box<dyn Module>>,
1346            &mut S,
1347        ) -> Result<()>,
1348        include_traced: bool,
1349        include_unused: bool,
1350    ) -> Result<()> {
1351        self.traverse_edges_dfs_impl::<S>(
1352            entries,
1353            state,
1354            visit_preorder,
1355            visit_postorder,
1356            Direction::Outgoing,
1357            include_traced,
1358            include_unused,
1359        )
1360    }
1361
1362    /// Traverses all reachable edges in dfs order over the reversed graph. The preorder visitor can
1363    /// be used to forward state up the graph, and to skip subgraphs
1364    ///
1365    /// Target nodes can be revisited (once per incoming edge) in the preorder_visitor, in the post
1366    /// order visitor they are visited exactly once with the first edge they were discovered with.
1367    /// Edges are traversed in normal order, so should correspond to reference order.
1368    ///
1369    /// * `entries` - The entry modules to start the traversal from
1370    /// * `state` - The state to be passed to the visitors
1371    /// * `visit_preorder` - Called before visiting the children of a node.
1372    ///    - Receives: (originating &SingleModuleGraphNode, edge &ChunkingType), target
1373    ///      &SingleModuleGraphNode, state &S
1374    ///    - Can return [GraphTraversalAction]s to control the traversal
1375    /// * `visit_postorder` - Called after visiting the parents of a node. Return
1376    ///    - Receives: (originating &SingleModuleGraphNode, edge &ChunkingType), target
1377    ///      &SingleModuleGraphNode, state &S
1378    pub fn traverse_edges_reverse_dfs<S>(
1379        &self,
1380        entries: impl IntoIterator<Item = ResolvedVc<Box<dyn Module>>>,
1381        state: &mut S,
1382        visit_preorder: impl FnMut(
1383            Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData)>,
1384            ResolvedVc<Box<dyn Module>>,
1385            &mut S,
1386        ) -> Result<GraphTraversalAction>,
1387        visit_postorder: impl FnMut(
1388            Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData)>,
1389            ResolvedVc<Box<dyn Module>>,
1390            &mut S,
1391        ) -> Result<()>,
1392    ) -> Result<()> {
1393        self.traverse_edges_dfs_impl::<S>(
1394            entries,
1395            state,
1396            visit_preorder,
1397            visit_postorder,
1398            Direction::Incoming,
1399            false,
1400            false,
1401        )
1402    }
1403
1404    fn traverse_edges_dfs_impl<S>(
1405        &self,
1406        entries: impl IntoIterator<Item = ResolvedVc<Box<dyn Module>>>,
1407        state: &mut S,
1408        mut visit_preorder: impl FnMut(
1409            Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData)>,
1410            ResolvedVc<Box<dyn Module>>,
1411            &mut S,
1412        ) -> Result<GraphTraversalAction>,
1413        mut visit_postorder: impl FnMut(
1414            Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData)>,
1415            ResolvedVc<Box<dyn Module>>,
1416            &mut S,
1417        ) -> Result<()>,
1418        direction: Direction,
1419        include_traced: bool,
1420        include_unused: bool,
1421    ) -> Result<()> {
1422        if direction == Direction::Incoming {
1423            debug_assert!(
1424                self.skip_visited_module_children,
1425                "Can only trace reverse edges in a single layer graph. We do not model cross \
1426                 graph reverse edges"
1427            );
1428        }
1429        let entries = entries.into_iter().collect::<Vec<_>>();
1430
1431        enum Pass {
1432            Visit,
1433            ExpandAndVisit,
1434        }
1435        #[allow(clippy::type_complexity)] // This is a temporary internal structure
1436        let mut stack: Vec<(
1437            Pass,
1438            Option<(GraphNodeIndex, GraphEdgeIndex)>,
1439            GraphNodeIndex,
1440        )> = Vec::with_capacity(entries.len());
1441        for entry in entries.into_iter().rev() {
1442            stack.push((Pass::ExpandAndVisit, None, self.get_entry(entry)?));
1443        }
1444        let mut expanded = FxHashSet::default();
1445        while let Some((pass, parent, current)) = stack.pop() {
1446            let parent_arg = match parent {
1447                Some((parent_node, parent_edge)) => Some((
1448                    self.get_node(parent_node)?.module(),
1449                    self.get_edge(parent_edge)?,
1450                )),
1451                None => None,
1452            };
1453            let current_node = self.get_node(current)?;
1454            match pass {
1455                Pass::Visit => {
1456                    visit_postorder(parent_arg, current_node.module(), state)?;
1457                }
1458                Pass::ExpandAndVisit => {
1459                    let action = visit_preorder(parent_arg, current_node.module(), state)?;
1460                    if action == GraphTraversalAction::Exclude {
1461                        continue;
1462                    }
1463                    stack.push((Pass::Visit, parent, current));
1464                    if action == GraphTraversalAction::Continue
1465                        && expanded.insert(current)
1466                        && self.should_visit_node(current_node, direction)
1467                    {
1468                        let current = current_node.target_idx(direction).unwrap_or(current);
1469                        stack.extend(
1470                            self.iter_graphs_neighbors_rev(
1471                                current,
1472                                direction,
1473                                include_traced,
1474                                include_unused,
1475                            )
1476                            .map(|(edge, child)| {
1477                                (Pass::ExpandAndVisit, Some((current, edge)), child)
1478                            }),
1479                        );
1480                    }
1481                }
1482            }
1483        }
1484
1485        Ok(())
1486    }
1487
1488    /// Traverse all cycles in the graph (where the edge filter returns true for the whole cycle)
1489    /// and call the visitor with the nodes in the cycle.
1490    /// Notably, module self-references are also treated as cycles.
1491    pub fn traverse_cycles(
1492        &self,
1493        edge_filter: impl Fn(&RefData) -> bool,
1494        mut visit_cycle: impl FnMut(&[&ResolvedVc<Box<dyn Module>>]) -> Result<()>,
1495    ) -> Result<()> {
1496        for (graph_idx, graph) in self.graphs.iter().enumerate() {
1497            graph.traverse_cycles(
1498                &edge_filter,
1499                &mut visit_cycle,
1500                graph_idx as u32,
1501                &self.unused_references_edges,
1502            )?;
1503        }
1504        Ok(())
1505    }
1506
1507    /// Computes a priority for [`Self::traverse_edges_fixed_point_with_priority`] that
1508    /// minimizes the number of retraversals, as the *reverse postorder* of a depth-first search
1509    /// from `entries`.
1510    ///
1511    /// Every module reachable from `entries` (following the same edges as the fixed point
1512    /// traversal: traced and unused references are ignored) gets a unique rank. For every
1513    /// reference that is not a back edge of the depth-first search (every reference outside of a
1514    /// cycle is never a back edge), the importing module has a lower rank than the imported
1515    /// module. When the graph has no cycles, visiting lower ranks first (e.g. using
1516    /// [`std::cmp::Reverse`] as the priority, as the traversal visits high priorities first)
1517    /// therefore visits every module only after all of the modules importing it, so each module is
1518    /// visited exactly once. In contrast to using the BFS depth, a module is not revisited just
1519    /// because a longer path to it is processed later. Modules in or after a cycle can still be
1520    /// visited multiple times.
1521    ///
1522    /// Runs in linear time and without recursion.
1523    pub fn reverse_postorder_ranks(
1524        &self,
1525        entries: impl IntoIterator<Item = ResolvedVc<Box<dyn Module>>>,
1526    ) -> Result<FxHashMap<ResolvedVc<Box<dyn Module>>, usize>> {
1527        // Nodes are identified by their canonical node (i.e. never a `VisitedModule`), which is
1528        // the node the fixed point traversal expands.
1529        let canonical = |node: GraphNodeIndex| -> Result<GraphNodeIndex> {
1530            Ok(self
1531                .get_node(node)?
1532                .target_idx(Direction::Outgoing)
1533                .unwrap_or(node))
1534        };
1535
1536        let mut visited: FxHashSet<GraphNodeIndex> = FxHashSet::default();
1537        let mut postorder: Vec<GraphNodeIndex> = Vec::new();
1538        for entry in entries {
1539            let entry = canonical(self.get_entry(entry)?)?;
1540            if !visited.insert(entry) {
1541                continue;
1542            }
1543            let mut stack = vec![(
1544                entry,
1545                self.iter_graphs_neighbors_rev(entry, Direction::Outgoing, false, false),
1546            )];
1547            while let Some((node, children)) = stack.last_mut() {
1548                if let Some((_, child)) = children.next() {
1549                    let child = canonical(child)?;
1550                    if visited.insert(child) {
1551                        stack.push((
1552                            child,
1553                            self.iter_graphs_neighbors_rev(
1554                                child,
1555                                Direction::Outgoing,
1556                                false,
1557                                false,
1558                            ),
1559                        ));
1560                    }
1561                } else {
1562                    postorder.push(*node);
1563                    stack.pop();
1564                }
1565            }
1566        }
1567
1568        let count = postorder.len();
1569        postorder
1570            .into_iter()
1571            .enumerate()
1572            .map(|(i, node)| Ok((self.get_node(node)?.module(), count - 1 - i)))
1573            .collect()
1574    }
1575
1576    /// Traverses all reachable nodes and also continue revisiting them as long the visitor returns
1577    /// GraphTraversalAction::Continue. The visitor is responsible for the runtime complexity and
1578    /// eventual termination of the traversal. This corresponds to computing a fixed point state for
1579    /// the graph.
1580    ///
1581    /// Nodes are (re)visited according to the returned priority of the node, prioritizing high
1582    /// values. This priority is intended to be used a heuristic to reduce the number of
1583    /// retraversals. [`ModuleGraphSnapshot::reverse_postorder_ranks`] computes a priority that
1584    /// avoids retraversals for all modules not in or after a cycle.
1585    ///
1586    /// * `entries` - The entry modules to start the traversal from
1587    /// * `state` - The state to be passed to the callbacks
1588    /// * `visit` - Called for a specific edge
1589    ///    - Receives: (originating &SingleModuleGraphNode, edge &ChunkingType), target
1590    ///      &SingleModuleGraphNode, state &S
1591    ///    - Return [GraphTraversalAction]s to control the traversal
1592    /// * `priority` - Called for before visiting the children of a node to determine its priority.
1593    ///    - Receives: target &SingleModuleGraphNode, state &S
1594    ///    - Return a priority value for the node
1595    ///
1596    /// Returns the number of node visits (i.e. higher than the node count if there are
1597    /// retraversals).
1598    pub fn traverse_edges_fixed_point_with_priority<'graph, S, P: Ord>(
1599        &'graph self,
1600        entries: impl IntoIterator<Item = (ResolvedVc<Box<dyn Module>>, P)>,
1601        state: &mut S,
1602        mut visit: impl FnMut(
1603            Option<(ResolvedVc<Box<dyn Module>>, &'graph RefData, GraphEdgeIndex)>,
1604            ResolvedVc<Box<dyn Module>>,
1605            GraphNodeIndex,
1606            &mut S,
1607        ) -> Result<GraphTraversalAction>,
1608        priority: impl Fn(ResolvedVc<Box<dyn Module>>, &mut S) -> Result<P>,
1609    ) -> Result<usize> {
1610        if self.skip_visited_module_children {
1611            panic!(
1612                "traverse_edges_fixed_point_with_priority musn't be called on individual graphs"
1613            );
1614        }
1615
1616        let mut visit_order = 0usize;
1617        let mut order = || {
1618            let order = visit_order;
1619            visit_order += 1;
1620            order
1621        };
1622        #[derive(PartialEq, Eq)]
1623        struct NodeWithPriority<T: Ord> {
1624            node: GraphNodeIndex,
1625            priority: T,
1626            visit_order: usize,
1627        }
1628        impl<T: Ord> PartialOrd for NodeWithPriority<T> {
1629            fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1630                Some(self.cmp(other))
1631            }
1632        }
1633        impl<T: Ord> Ord for NodeWithPriority<T> {
1634            fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1635                // BinaryHeap prioritizes high values
1636
1637                self.priority
1638                    .cmp(&other.priority)
1639                    // Use visit_order, so when there are ties we prioritize earlier discovered
1640                    // nodes, reverting to a BFS in the the case where all priorities are equal
1641                    .then(self.visit_order.cmp(&other.visit_order))
1642            }
1643        }
1644
1645        let mut queue_set = FxHashSet::default();
1646        let mut queue = BinaryHeap::new();
1647        for (m, priority) in entries {
1648            let node = self.get_entry(m)?;
1649            // Entries are tracked in `queue_set` like any other queued node, so that an entry
1650            // that is also reachable from another entry isn't queued (and visited) twice, and
1651            // duplicate entries are ignored.
1652            if queue_set.insert(node) {
1653                queue.push(NodeWithPriority {
1654                    node,
1655                    priority,
1656                    visit_order: order(),
1657                });
1658            }
1659        }
1660
1661        for entry_node in &queue {
1662            visit(
1663                None,
1664                self.get_node(entry_node.node)?.module(),
1665                entry_node.node,
1666                state,
1667            )?;
1668        }
1669
1670        let mut visit_count = 0usize;
1671        while let Some(NodeWithPriority { node, .. }) = queue.pop() {
1672            queue_set.remove(&node);
1673            let node_weight = self.get_node(node)?;
1674            let node = node_weight.target_idx(Direction::Outgoing).unwrap_or(node);
1675
1676            visit_count += 1;
1677
1678            for (edge, succ) in
1679                self.iter_graphs_neighbors_rev(node, Direction::Outgoing, false, false)
1680            {
1681                let succ_weight = self.get_node(succ)?;
1682
1683                let action = visit(
1684                    Some((node_weight.module(), self.get_edge(edge)?, edge)),
1685                    succ_weight.module(),
1686                    succ,
1687                    state,
1688                )?;
1689
1690                let succ = succ_weight.target_idx(Direction::Outgoing).unwrap_or(succ);
1691                if action == GraphTraversalAction::Continue && queue_set.insert(succ) {
1692                    queue.push(NodeWithPriority {
1693                        node: succ,
1694                        priority: priority(succ_weight.module(), state)?,
1695                        visit_order: order(),
1696                    });
1697                }
1698            }
1699        }
1700
1701        Ok(visit_count)
1702    }
1703
1704    /// Iterates all reachable modules in the graph, ignoring unused and traced references.
1705    pub fn iter_reachable_modules(
1706        &self,
1707    ) -> Result<impl Iterator<Item = ResolvedVc<Box<dyn Module>>>> {
1708        Ok(self.iter_reachable_nodes()?.filter_map(|n| match n {
1709            SingleModuleGraphNode::Module(m) => Some(*m),
1710            SingleModuleGraphNode::VisitedModule { .. } => None,
1711        }))
1712    }
1713
1714    /// Iterates all reachable nodes in the graph, ignoring unused and traced references.
1715    /// This includes VisitedModule nodes (which means that some modules are returned twice).
1716    pub fn iter_reachable_nodes<'a>(
1717        &'a self,
1718    ) -> Result<impl Iterator<Item = &'a SingleModuleGraphNode> + 'a> {
1719        ModuleGraphSnapshotNodeIterator::new(self)
1720    }
1721}
1722
1723struct ModuleGraphSnapshotNodeIterator<'a> {
1724    graph: &'a ModuleGraphSnapshot,
1725    visited: FxHashSet<GraphNodeIndex>,
1726    visit_queue: VecDeque<GraphNodeIndex>,
1727}
1728
1729impl<'a> ModuleGraphSnapshotNodeIterator<'a> {
1730    fn new(graph: &'a ModuleGraphSnapshot) -> Result<Self> {
1731        let entries = graph
1732            .graphs
1733            .iter()
1734            .flat_map(|g| g.chunk_group_modules())
1735            .map(|e| graph.get_entry(e))
1736            .collect::<Result<VecDeque<_>>>()?;
1737
1738        Ok(Self {
1739            graph,
1740            visited: FxHashSet::default(),
1741            visit_queue: entries,
1742        })
1743    }
1744}
1745impl<'a> Iterator for ModuleGraphSnapshotNodeIterator<'a> {
1746    type Item = &'a SingleModuleGraphNode;
1747
1748    fn next(&mut self) -> Option<Self::Item> {
1749        while let Some(node_idx) = self.visit_queue.pop_front() {
1750            if self.visited.insert(node_idx) {
1751                let node_weight = self.graph.get_node(node_idx).unwrap();
1752                if self
1753                    .graph
1754                    .should_visit_node(node_weight, Direction::Outgoing)
1755                {
1756                    let node = node_weight
1757                        .target_idx(Direction::Outgoing)
1758                        .unwrap_or(node_idx);
1759                    self.visit_queue.extend(
1760                        self.graph
1761                            .iter_graphs_neighbors_rev(node, Direction::Outgoing, false, false)
1762                            .map(|(_, succ)| succ)
1763                            .filter(|succ| !self.visited.contains(succ)),
1764                    );
1765                }
1766                return Some(node_weight);
1767            }
1768        }
1769        None
1770    }
1771}
1772impl FusedIterator for ModuleGraphSnapshotNodeIterator<'_> {}
1773
1774#[turbo_tasks::value_impl]
1775impl SingleModuleGraph {
1776    #[turbo_tasks::function(operation)]
1777    pub async fn new_with_entry(
1778        entry: ChunkGroupEntry,
1779        include_traced: bool,
1780        include_binding_usage: bool,
1781    ) -> Result<Vc<Self>> {
1782        SingleModuleGraph::new_inner(
1783            &GraphEntries::from_chunk_groups(vec![entry]),
1784            &Default::default(),
1785            include_traced,
1786            include_binding_usage,
1787        )
1788        .await
1789    }
1790
1791    #[turbo_tasks::function(operation)]
1792    pub async fn new_with_entries(
1793        entries: ResolvedVc<GraphEntries>,
1794        include_traced: bool,
1795        include_binding_usage: bool,
1796    ) -> Result<Vc<Self>> {
1797        SingleModuleGraph::new_inner(
1798            &*entries.await?,
1799            &Default::default(),
1800            include_traced,
1801            include_binding_usage,
1802        )
1803        .await
1804    }
1805
1806    #[turbo_tasks::function(operation)]
1807    pub async fn new_with_entries_visited(
1808        entries: ResolvedVc<GraphEntries>,
1809        visited_modules: OperationVc<VisitedModules>,
1810        include_traced: bool,
1811        include_binding_usage: bool,
1812    ) -> Result<Vc<Self>> {
1813        SingleModuleGraph::new_inner(
1814            &*entries.await?,
1815            &visited_modules.connect().await?.modules,
1816            include_traced,
1817            include_binding_usage,
1818        )
1819        .await
1820    }
1821
1822    #[turbo_tasks::function(operation)]
1823    pub async fn new_with_entries_visited_intern(
1824        // This must not be a Vc<Vec<_>> to ensure layout segment optimization hits the cache
1825        entries: GraphEntries,
1826        visited_modules: OperationVc<VisitedModules>,
1827        include_traced: bool,
1828        include_binding_usage: bool,
1829    ) -> Result<Vc<Self>> {
1830        SingleModuleGraph::new_inner(
1831            &entries,
1832            &visited_modules.connect().await?.modules,
1833            include_traced,
1834            include_binding_usage,
1835        )
1836        .await
1837    }
1838
1839    #[turbo_tasks::function]
1840    pub async fn module_count(&self) -> Vc<u64> {
1841        Vc::cell(self.number_of_modules as u64)
1842    }
1843
1844    #[turbo_tasks::function]
1845    pub async fn edge_count(&self) -> Vc<u64> {
1846        Vc::cell(self.graph.edge_count() as u64)
1847    }
1848}
1849
1850#[derive(Clone, Debug, Serialize, Deserialize, NonLocalValue)]
1851pub enum SingleModuleGraphNode {
1852    Module(ResolvedVc<Box<dyn Module>>),
1853    // Models a module that is referenced but has already been visited by an earlier graph.
1854    VisitedModule {
1855        idx: GraphNodeIndex,
1856        module: ResolvedVc<Box<dyn Module>>,
1857    },
1858}
1859
1860impl SingleModuleGraphNode {
1861    pub fn module(&self) -> ResolvedVc<Box<dyn Module>> {
1862        match self {
1863            SingleModuleGraphNode::Module(module) => *module,
1864            SingleModuleGraphNode::VisitedModule { module, .. } => *module,
1865        }
1866    }
1867    pub fn target_idx(&self, direction: Direction) -> Option<GraphNodeIndex> {
1868        match self {
1869            SingleModuleGraphNode::VisitedModule { idx, .. } => match direction {
1870                Direction::Outgoing => Some(*idx),
1871                Direction::Incoming => None,
1872            },
1873            SingleModuleGraphNode::Module(_) => None,
1874        }
1875    }
1876}
1877
1878#[derive(PartialEq, Eq, Debug)]
1879pub enum GraphTraversalAction {
1880    /// Continue visiting children
1881    Continue,
1882    /// Skip the immediate children, but visit the node in postorder
1883    Skip,
1884    /// Skip the immediate children and the node in postorder
1885    Exclude,
1886}
1887
1888// These nodes are created while walking the Turbopack modules references, and are used to then
1889// afterwards build the SingleModuleGraph.
1890#[derive(Clone, Hash, PartialEq, Eq)]
1891enum SingleModuleGraphBuilderNode {
1892    /// A regular module
1893    Module {
1894        module: ResolvedVc<Box<dyn Module>>,
1895        /// module.ident().to_string(), eagerly computed for tracing, otherwise None
1896        ident: Option<ReadRef<RcStr>>,
1897        /// whether this module is a tracing context
1898        is_traced: bool,
1899    },
1900    /// A reference to a module that is already listed in visited_modules
1901    VisitedModule {
1902        module: ResolvedVc<Box<dyn Module>>,
1903        idx: GraphNodeIndex,
1904    },
1905}
1906
1907impl SingleModuleGraphBuilderNode {
1908    async fn new_module(
1909        emit_spans: bool,
1910        module: ResolvedVc<Box<dyn Module>>,
1911        is_traced: bool,
1912    ) -> Result<Self> {
1913        Ok(Self::Module {
1914            module,
1915            ident: if emit_spans {
1916                // INVALIDATION: we don't need to invalidate when the span name changes
1917                Some(module.ident_string().untracked().await?)
1918            } else {
1919                None
1920            },
1921            is_traced,
1922        })
1923    }
1924    fn new_visited_module(module: ResolvedVc<Box<dyn Module>>, idx: GraphNodeIndex) -> Self {
1925        Self::VisitedModule { module, idx }
1926    }
1927}
1928
1929struct SingleModuleGraphBuilder<'a> {
1930    visited_modules: &'a FxIndexMap<ResolvedVc<Box<dyn Module>>, GraphNodeIndex>,
1931
1932    emit_spans: bool,
1933
1934    /// Whether to walk ChunkingType::Traced references
1935    include_traced: bool,
1936
1937    /// Whether to read ModuleReference::binding_usage()
1938    include_binding_usage: bool,
1939}
1940impl Visit<SingleModuleGraphBuilderNode, RefData> for SingleModuleGraphBuilder<'_> {
1941    type EdgesIntoIter = Vec<(SingleModuleGraphBuilderNode, RefData)>;
1942    type EdgesFuture = impl Future<Output = Result<Self::EdgesIntoIter>>;
1943
1944    fn visit(
1945        &mut self,
1946        node: &SingleModuleGraphBuilderNode,
1947        _edge: Option<&RefData>,
1948    ) -> VisitControlFlow {
1949        match node {
1950            SingleModuleGraphBuilderNode::Module { .. } => VisitControlFlow::Continue,
1951            // Module was already visited previously
1952            SingleModuleGraphBuilderNode::VisitedModule { .. } => VisitControlFlow::Skip,
1953        }
1954    }
1955
1956    fn edges(&mut self, node: &SingleModuleGraphBuilderNode) -> Self::EdgesFuture {
1957        // Destructure beforehand to not have to clone the whole node when entering the async block
1958        let &SingleModuleGraphBuilderNode::Module {
1959            module, is_traced, ..
1960        } = node
1961        else {
1962            // These are always skipped in `visit()`
1963            unreachable!()
1964        };
1965        let visited_modules = self.visited_modules;
1966        let emit_spans = self.emit_spans;
1967        let include_traced = self.include_traced;
1968        let include_binding_usage = self.include_binding_usage;
1969        async move {
1970            let refs_cell = if !is_traced {
1971                primary_chunkable_referenced_modules(*module, include_traced, include_binding_usage)
1972            } else {
1973                // Currently we don't care about the binding usage of traced references
1974                referenced_modules_and_affecting_sources(*module, false)
1975            };
1976            let refs = match refs_cell.await {
1977                Ok(refs) => refs,
1978                Err(e) => {
1979                    return Err(e.context(module.ident().to_string().await?));
1980                }
1981            };
1982
1983            refs.iter()
1984                .flat_map(|(reference, resolved)| {
1985                    resolved.modules.iter().map(|m| {
1986                        (
1987                            *reference,
1988                            resolved.chunking_type.clone(),
1989                            resolved.binding_usage.clone(),
1990                            *m,
1991                        )
1992                    })
1993                })
1994                .filter(|(_, ty, _, _)| {
1995                    // Ignore non-entry traced reference if not already in tracing mode.
1996                    //
1997                    // ChunkingType::Traced{TracedMode::Entry}
1998                    // ==> target is always traced
1999                    // ChunkingType::Traced{TracedMode::Transitive}
2000                    // ==> target only traced if parent is traced
2001                    // ChunkingType::*
2002                    // ==> target only traced if parent is traced
2003                    !matches!(
2004                        ty,
2005                        ChunkingType::Traced {
2006                            mode: TracedMode::Transitive
2007                        }
2008                    ) || is_traced
2009                })
2010                .map(async |(reference, ty, binding_usage, target)| {
2011                    let to = if let Some(idx) = visited_modules.get(&target) {
2012                        SingleModuleGraphBuilderNode::new_visited_module(target, *idx)
2013                    } else {
2014                        SingleModuleGraphBuilderNode::new_module(
2015                            emit_spans,
2016                            target,
2017                            is_traced || ty.is_traced(),
2018                        )
2019                        .await?
2020                    };
2021                    Ok((
2022                        to,
2023                        RefData {
2024                            chunking_type: ty,
2025                            binding_usage,
2026                            reference,
2027                        },
2028                    ))
2029                })
2030                .try_join()
2031                .await
2032        }
2033    }
2034
2035    fn span(
2036        &mut self,
2037        node: &SingleModuleGraphBuilderNode,
2038        edge: Option<&RefData>,
2039    ) -> tracing::Span {
2040        if !self.emit_spans {
2041            return Span::none();
2042        }
2043
2044        let mut span = match node {
2045            SingleModuleGraphBuilderNode::Module {
2046                ident: Some(ident), ..
2047            } => {
2048                tracing::info_span!("module", name = display(ident))
2049            }
2050            SingleModuleGraphBuilderNode::VisitedModule { .. } => {
2051                tracing::info_span!("visited module")
2052            }
2053            _ => unreachable!(),
2054        };
2055
2056        if let Some(edge) = edge {
2057            match &edge.chunking_type {
2058                ChunkingType::Parallel {
2059                    inherit_async: _,
2060                    hoisted: _,
2061                } => {}
2062                ChunkingType::Traced { .. } => {
2063                    let _span = span.entered();
2064                    span = tracing::info_span!("traced reference");
2065                }
2066                ChunkingType::Async => {
2067                    let _span = span.entered();
2068                    span = tracing::info_span!("async reference");
2069                }
2070                ChunkingType::PerEntry => {
2071                    let _span = span.entered();
2072                    span = tracing::info_span!("per-entry reference");
2073                }
2074                ChunkingType::Emitted { namespace, .. } => {
2075                    let _span = span.entered();
2076                    span = tracing::info_span!("emitted reference", namespace = debug(&namespace));
2077                }
2078                ChunkingType::Collected { namespace, .. } => {
2079                    let _span = span.entered();
2080                    span =
2081                        tracing::info_span!("collected reference", namespace = debug(&namespace));
2082                }
2083                ChunkingType::Isolated { _ty: ty, merge_tag } => {
2084                    let _span = span.entered();
2085                    span = tracing::info_span!(
2086                        "isolated reference",
2087                        ty = debug(&ty),
2088                        merge_tag = debug(&merge_tag)
2089                    );
2090                }
2091                ChunkingType::Shared {
2092                    inherit_async: _,
2093                    merge_tag,
2094                } => {
2095                    let _span = span.entered();
2096                    span = tracing::info_span!("shared reference", merge_tag = debug(&merge_tag));
2097                }
2098            };
2099        }
2100
2101        span
2102    }
2103}
2104
2105#[cfg(test)]
2106pub mod tests {
2107    use anyhow::Result;
2108    use rustc_hash::FxHashMap;
2109    use turbo_rcstr::{RcStr, rcstr};
2110    use turbo_tasks::{ReadRef, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToString, Vc};
2111    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
2112    use turbo_tasks_fs::{FileSystem, FileSystemPath, VirtualFileSystem};
2113
2114    use super::*;
2115    use crate::{
2116        asset::{Asset, AssetContent},
2117        ident::AssetIdent,
2118        issue::{CollectibleIssuesExt, IssueSeverity},
2119        module::{Module, ModuleSideEffects},
2120        module_graph::chunk_group_info::{ChunkGroup, EntryHeuristics},
2121        reference::{ModuleReference, ModuleReferences},
2122        resolve::ModuleResolveResult,
2123    };
2124
2125    #[turbo_tasks::value(shared)]
2126    struct ImportTraceTestResult {
2127        has_entry: bool,
2128        traces: Vec<Vec<RcStr>>,
2129        missing_traces: Vec<Vec<RcStr>>,
2130    }
2131
2132    #[turbo_tasks::function(operation, root)]
2133    async fn import_trace_test_operation(rootless: bool) -> Result<Vc<ImportTraceTestResult>> {
2134        let fs = VirtualFileSystem::new_with_name(rcstr!("test"));
2135        let root = fs.root().await?;
2136        let repo = TestRepo::new(
2137            &root,
2138            [
2139                ("entry.js", vec!["dependency.js"]),
2140                ("dependency.js", vec!["entry.js"]),
2141            ],
2142        );
2143        let entry = Vc::upcast::<Box<dyn Module>>(MockModule::new(root.join("entry.js")?, repo))
2144            .to_resolved()
2145            .await?;
2146        let graph = SingleModuleGraph::new_with_entries(
2147            GraphEntries::resolved_cell(GraphEntries::new(
2148                vec![ChunkGroupEntry::Entry {
2149                    modules: vec![entry],
2150                    heuristics: EntryHeuristics::default(),
2151                }],
2152                vec![],
2153            )),
2154            false,
2155            false,
2156        )
2157        .connect()
2158        .to_resolved()
2159        .await?;
2160        let graph = if rootless {
2161            // Initialize the source graph's cache before cloning to ensure clones reset derived
2162            // state instead of retaining entry indices that could become stale after modification.
2163            let _ = graph.await?.entry_nodes();
2164            let mut graph = (*graph.await?).clone();
2165            graph.entries = GraphEntries::default();
2166            graph.resolved_cell()
2167        } else {
2168            graph
2169        };
2170
2171        let has_entry = graph.await?.has_entry_module(entry);
2172        let tracer = ModuleGraphImportTracer::new(*graph);
2173        let traces = tracer
2174            .get_traces(root.join("dependency.js")?)
2175            .await?
2176            .0
2177            .iter()
2178            .map(|trace| trace.iter().map(|ident| ident.path.path.clone()).collect())
2179            .collect();
2180        let missing_traces = tracer
2181            .get_traces(root.join("missing.js")?)
2182            .await?
2183            .0
2184            .iter()
2185            .map(|trace| trace.iter().map(|ident| ident.path.path.clone()).collect())
2186            .collect();
2187
2188        Ok(ImportTraceTestResult {
2189            has_entry,
2190            traces,
2191            missing_traces,
2192        }
2193        .cell())
2194    }
2195
2196    #[turbo_tasks::value(shared)]
2197    struct ImportTraceIssues {
2198        issues: Vec<(IssueSeverity, RcStr)>,
2199    }
2200
2201    #[turbo_tasks::function(operation, root)]
2202    async fn import_trace_issues_operation(
2203        trace_operation: OperationVc<ImportTraceTestResult>,
2204    ) -> Result<Vc<ImportTraceIssues>> {
2205        let _ = trace_operation.connect().await?;
2206        let issues = trace_operation
2207            .peek_issues()
2208            .iter()
2209            .map(async |issue| {
2210                let issue = issue.into_trait_ref().await?;
2211                Ok((
2212                    issue.severity(),
2213                    issue.title().await?.to_unstyled_string().into(),
2214                ))
2215            })
2216            .try_join()
2217            .await?;
2218        Ok(ImportTraceIssues { issues }.cell())
2219    }
2220
2221    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2222    async fn test_import_trace_uses_explicit_entry_as_cycle_root() {
2223        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2224            BackendOptions::default(),
2225            noop_backing_storage(),
2226        ));
2227        tt.run_once(async {
2228            let result = import_trace_test_operation(false)
2229                .read_strongly_consistent()
2230                .await?;
2231            assert!(result.has_entry);
2232            assert_eq!(
2233                result.traces,
2234                vec![vec![rcstr!("dependency.js"), rcstr!("entry.js")]]
2235            );
2236            assert!(result.missing_traces.is_empty());
2237            Ok(())
2238        })
2239        .await
2240        .unwrap();
2241    }
2242
2243    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2244    async fn test_cloned_rootless_import_trace_resets_cache_and_emits_bug_issue() {
2245        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2246            BackendOptions::default(),
2247            noop_backing_storage(),
2248        ));
2249        tt.run_once(async {
2250            let trace_operation = import_trace_test_operation(true);
2251            let result = trace_operation.read_strongly_consistent().await?;
2252            assert!(!result.has_entry);
2253            assert_eq!(result.traces, vec![vec![rcstr!("dependency.js")]]);
2254            assert!(result.missing_traces.is_empty());
2255
2256            let issues = import_trace_issues_operation(trace_operation)
2257                .read_strongly_consistent()
2258                .await?;
2259            assert_eq!(
2260                issues.issues,
2261                vec![(
2262                    IssueSeverity::Bug,
2263                    rcstr!("Module graph is missing an entry point")
2264                )]
2265            );
2266            Ok(())
2267        })
2268        .await
2269        .unwrap();
2270    }
2271
2272    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2273    async fn test_traverse_dfs_from_entries_diamond() {
2274        run_graph_test(
2275            vec![rcstr!("a.js")],
2276            {
2277                let mut deps = FxHashMap::default();
2278                // A classic diamond dependency on d
2279                deps.insert(rcstr!("a.js"), vec![rcstr!("b.js"), rcstr!("c.js")]);
2280                deps.insert(rcstr!("b.js"), vec![rcstr!("d.js")]);
2281                deps.insert(rcstr!("c.js"), vec![rcstr!("d.js")]);
2282                deps
2283            },
2284            |graph, entry_modules, module_to_name| {
2285                let mut preorder_visits = Vec::new();
2286                let mut postorder_visits = Vec::new();
2287
2288                graph.traverse_edges_dfs(
2289                    entry_modules,
2290                    &mut (),
2291                    |parent, target, _| {
2292                        preorder_visits.push((
2293                            parent.map(|(node, _)| module_to_name.get(&node).unwrap().clone()),
2294                            module_to_name.get(&target).unwrap().clone(),
2295                        ));
2296                        Ok(GraphTraversalAction::Continue)
2297                    },
2298                    |parent, target, _| {
2299                        postorder_visits.push((
2300                            parent.map(|(node, _)| module_to_name.get(&node).unwrap().clone()),
2301                            module_to_name.get(&target).unwrap().clone(),
2302                        ));
2303                        Ok(())
2304                    },
2305                    false,
2306                    false,
2307                )?;
2308                assert_eq!(
2309                    vec![
2310                        (None, rcstr!("a.js")),
2311                        (Some(rcstr!("a.js")), rcstr!("b.js")),
2312                        (Some(rcstr!("b.js")), rcstr!("d.js")),
2313                        (Some(rcstr!("a.js")), rcstr!("c.js")),
2314                        (Some(rcstr!("c.js")), rcstr!("d.js"))
2315                    ],
2316                    preorder_visits
2317                );
2318                assert_eq!(
2319                    vec![
2320                        (Some(rcstr!("b.js")), rcstr!("d.js")),
2321                        (Some(rcstr!("a.js")), rcstr!("b.js")),
2322                        (Some(rcstr!("c.js")), rcstr!("d.js")),
2323                        (Some(rcstr!("a.js")), rcstr!("c.js")),
2324                        (None, rcstr!("a.js"))
2325                    ],
2326                    postorder_visits
2327                );
2328                Ok(())
2329            },
2330        )
2331        .await;
2332    }
2333
2334    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2335    async fn test_traverse_dfs_from_entries_cycle() {
2336        run_graph_test(
2337            vec![rcstr!("a.js")],
2338            {
2339                let mut deps = FxHashMap::default();
2340                // A cycle of length 3
2341                deps.insert(rcstr!("a.js"), vec![rcstr!("b.js")]);
2342                deps.insert(rcstr!("b.js"), vec![rcstr!("c.js")]);
2343                deps.insert(rcstr!("c.js"), vec![rcstr!("a.js")]);
2344                deps
2345            },
2346            |graph, entry_modules, module_to_name| {
2347                let mut preorder_visits = Vec::new();
2348                let mut postorder_visits = Vec::new();
2349
2350                graph.traverse_edges_dfs(
2351                    entry_modules,
2352                    &mut (),
2353                    |parent, target, _| {
2354                        preorder_visits.push((
2355                            parent.map(|(node, _)| module_to_name.get(&node).unwrap().clone()),
2356                            module_to_name.get(&target).unwrap().clone(),
2357                        ));
2358                        Ok(GraphTraversalAction::Continue)
2359                    },
2360                    |parent, target, _| {
2361                        postorder_visits.push((
2362                            parent.map(|(node, _)| module_to_name.get(&node).unwrap().clone()),
2363                            module_to_name.get(&target).unwrap().clone(),
2364                        ));
2365                        Ok(())
2366                    },
2367                    false,
2368                    false,
2369                )?;
2370                assert_eq!(
2371                    vec![
2372                        (None, rcstr!("a.js")),
2373                        (Some(rcstr!("a.js")), rcstr!("b.js")),
2374                        (Some(rcstr!("b.js")), rcstr!("c.js")),
2375                        (Some(rcstr!("c.js")), rcstr!("a.js")),
2376                    ],
2377                    preorder_visits
2378                );
2379                assert_eq!(
2380                    vec![
2381                        (Some(rcstr!("c.js")), rcstr!("a.js")),
2382                        (Some(rcstr!("b.js")), rcstr!("c.js")),
2383                        (Some(rcstr!("a.js")), rcstr!("b.js")),
2384                        (None, rcstr!("a.js"))
2385                    ],
2386                    postorder_visits
2387                );
2388                Ok(())
2389            },
2390        )
2391        .await;
2392    }
2393
2394    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2395    async fn test_traverse_edges_fixed_point_with_priority_cycle() {
2396        run_graph_test(
2397            vec![rcstr!("a.js")],
2398            {
2399                let mut deps = FxHashMap::default();
2400                // A cycle of length 3
2401                deps.insert(rcstr!("a.js"), vec![rcstr!("b.js")]);
2402                deps.insert(rcstr!("b.js"), vec![rcstr!("c.js")]);
2403                deps.insert(rcstr!("c.js"), vec![rcstr!("a.js")]);
2404                deps
2405            },
2406            |graph, entry_modules, module_to_name| {
2407                let mut visits = Vec::new();
2408                let mut count = 0;
2409
2410                graph.traverse_edges_fixed_point_with_priority(
2411                    entry_modules.into_iter().map(|m| (m, 0)),
2412                    &mut (),
2413                    |parent, target, _, _| {
2414                        visits.push((
2415                            parent.map(|(node, _, _)| module_to_name.get(&node).unwrap().clone()),
2416                            module_to_name.get(&target).unwrap().clone(),
2417                        ));
2418                        count += 1;
2419
2420                        // We are a cycle so we need to break the loop eventually
2421                        Ok(if count < 6 {
2422                            GraphTraversalAction::Continue
2423                        } else {
2424                            GraphTraversalAction::Skip
2425                        })
2426                    },
2427                    |_, _| Ok(0),
2428                )?;
2429                assert_eq!(
2430                    vec![
2431                        (None, rcstr!("a.js")),
2432                        (Some(rcstr!("a.js")), rcstr!("b.js")),
2433                        (Some(rcstr!("b.js")), rcstr!("c.js")),
2434                        (Some(rcstr!("c.js")), rcstr!("a.js")),
2435                        // we start following the cycle again
2436                        (Some(rcstr!("a.js")), rcstr!("b.js")),
2437                        (Some(rcstr!("b.js")), rcstr!("c.js")),
2438                    ],
2439                    visits
2440                );
2441
2442                Ok(())
2443            },
2444        )
2445        .await;
2446    }
2447
2448    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2449    async fn test_traverse_edges_fixed_point_no_priority_is_bfs() {
2450        run_graph_test(
2451            vec![rcstr!("a.js")],
2452            {
2453                let mut deps = FxHashMap::default();
2454                // a simple triangle
2455                //        a
2456                //      b   c
2457                //   d    e    f
2458                deps.insert(rcstr!("a.js"), vec![rcstr!("b.js"), rcstr!("c.js")]);
2459                deps.insert(rcstr!("b.js"), vec![rcstr!("d.js"), rcstr!("e.js")]);
2460                deps.insert(rcstr!("c.js"), vec![rcstr!("e.js"), rcstr!("f.js")]);
2461                deps
2462            },
2463            |graph, entry_modules, module_to_name| {
2464                let mut visits = Vec::new();
2465                let mut count = 0;
2466
2467                graph.traverse_edges_fixed_point_with_priority(
2468                    entry_modules.into_iter().map(|m| (m, 0)),
2469                    &mut (),
2470                    |parent, target, _, _| {
2471                        visits.push((
2472                            parent.map(|(node, _, _)| module_to_name.get(&node).unwrap().clone()),
2473                            module_to_name.get(&target).unwrap().clone(),
2474                        ));
2475                        count += 1;
2476
2477                        // We are a cycle so we need to break the loop eventually
2478                        Ok(if count < 6 {
2479                            GraphTraversalAction::Continue
2480                        } else {
2481                            GraphTraversalAction::Skip
2482                        })
2483                    },
2484                    |_, _| Ok(0),
2485                )?;
2486
2487                assert_eq!(
2488                    vec![
2489                        (None, rcstr!("a.js")),
2490                        (Some(rcstr!("a.js")), rcstr!("c.js")),
2491                        (Some(rcstr!("a.js")), rcstr!("b.js")),
2492                        (Some(rcstr!("b.js")), rcstr!("e.js")),
2493                        (Some(rcstr!("b.js")), rcstr!("d.js")),
2494                        (Some(rcstr!("c.js")), rcstr!("f.js")),
2495                        (Some(rcstr!("c.js")), rcstr!("e.js")),
2496                    ],
2497                    visits
2498                );
2499
2500                Ok(())
2501            },
2502        )
2503        .await;
2504    }
2505
2506    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2507    async fn test_traverse_cycles() {
2508        run_graph_test(
2509            vec![rcstr!("a.js")],
2510            {
2511                let mut deps = FxHashMap::default();
2512                // The cycles are: (i, j, k), and (s) which a self-import
2513                //          a
2514                //      /   |    \
2515                //     /i   s-\   x
2516                //     |j   \-/
2517                //     \k
2518                deps.insert(
2519                    rcstr!("a.js"),
2520                    vec![rcstr!("i.js"), rcstr!("s.js"), rcstr!("x.js")],
2521                );
2522                deps.insert(rcstr!("i.js"), vec![rcstr!("j.js")]);
2523                deps.insert(rcstr!("j.js"), vec![rcstr!("k.js")]);
2524                deps.insert(rcstr!("k.js"), vec![rcstr!("i.js")]);
2525                deps.insert(rcstr!("s.js"), vec![rcstr!("s.js")]);
2526                deps
2527            },
2528            |graph, _, module_to_name| {
2529                let mut cycles = vec![];
2530
2531                graph.traverse_cycles(
2532                    |_| true,
2533                    |cycle| {
2534                        cycles.push(
2535                            cycle
2536                                .iter()
2537                                .map(|n| module_to_name.get(*n).unwrap().clone())
2538                                .collect::<Vec<_>>(),
2539                        );
2540                        Ok(())
2541                    },
2542                )?;
2543
2544                assert_eq!(
2545                    cycles,
2546                    vec![
2547                        vec![rcstr!("k.js"), rcstr!("j.js"), rcstr!("i.js")],
2548                        vec![rcstr!("s.js")]
2549                    ],
2550                );
2551
2552                Ok(())
2553            },
2554        )
2555        .await;
2556    }
2557
2558    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2559    async fn test_reverse_edges_through_layered_graph() {
2560        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2561            BackendOptions::default(),
2562            noop_backing_storage(),
2563        ));
2564        tt.run_once(async move {
2565            #[turbo_tasks::value]
2566            struct ReverseTraversalResults {
2567                forward: Vec<RcStr>,
2568                reverse_from_d: Vec<RcStr>,
2569                reverse_from_b: Vec<RcStr>,
2570            }
2571
2572            #[turbo_tasks::function(operation, root)]
2573            async fn reverse_traversal_results_operation() -> Result<Vc<ReverseTraversalResults>> {
2574                let fs = VirtualFileSystem::new_with_name(rcstr!("test"));
2575                let root = fs.root().await?;
2576
2577                // a simple linear graph a -> b ->c
2578                // but b->c is in a parent graph and a is in the child
2579                let repo = TestRepo::new(
2580                    &root,
2581                    [("a.js", vec!["b.js", "d.js"]), ("b.js", vec!["c.js"])],
2582                );
2583                let make_module = |name| {
2584                    Vc::upcast::<Box<dyn Module>>(MockModule::new(root.join(name).unwrap(), repo))
2585                        .to_resolved()
2586                };
2587                let a_module = make_module("a.js").await?;
2588                let b_module = make_module("b.js").await?;
2589
2590                let parent_graph = SingleModuleGraph::new_with_entries(
2591                    GraphEntries::from_chunk_groups(vec![ChunkGroupEntry::Entry {
2592                        modules: vec![b_module],
2593                        heuristics: EntryHeuristics::default(),
2594                    }])
2595                    .resolved_cell(),
2596                    false,
2597                    false,
2598                );
2599
2600                let module_graph = ModuleGraph::from_graphs(
2601                    vec![
2602                        parent_graph,
2603                        SingleModuleGraph::new_with_entries_visited(
2604                            GraphEntries::from_chunk_groups(vec![ChunkGroupEntry::Entry {
2605                                modules: vec![a_module],
2606                                heuristics: EntryHeuristics::default(),
2607                            }])
2608                            .resolved_cell(),
2609                            VisitedModules::from_graph(parent_graph),
2610                            false,
2611                            false,
2612                        ),
2613                    ],
2614                    None,
2615                )
2616                .connect();
2617                let child_graph = module_graph
2618                    .iter_graphs()
2619                    .await?
2620                    .get(1)
2621                    .unwrap()
2622                    .connect()
2623                    .await?;
2624
2625                // test traversing forward from a in the child graph
2626                let mut visited_forward = Vec::new();
2627                child_graph.traverse_edges_dfs(
2628                    vec![a_module],
2629                    &mut (),
2630                    |_parent, child, _state_| {
2631                        visited_forward.push(child);
2632                        Ok(GraphTraversalAction::Continue)
2633                    },
2634                    |_, _, _| Ok(()),
2635                    false,
2636                    false,
2637                )?;
2638                let forward = visited_forward
2639                    .iter()
2640                    .map(|m| m.ident().to_string().owned())
2641                    .try_join()
2642                    .await?;
2643
2644                // test traversing backwards from 'd' which is only in the child graph
2645                let d_module = child_graph
2646                    .enumerate_nodes()
2647                    .map(async |(_index, module)| {
2648                        Ok(match module {
2649                            crate::module_graph::SingleModuleGraphNode::Module(module) => {
2650                                if module.ident().to_string().owned().await? == "[test]/d.js" {
2651                                    Some(*module)
2652                                } else {
2653                                    None
2654                                }
2655                            }
2656                            crate::module_graph::SingleModuleGraphNode::VisitedModule {
2657                                ..
2658                            } => None,
2659                        })
2660                    })
2661                    .try_flat_join()
2662                    .await?
2663                    .into_iter()
2664                    .next()
2665                    .unwrap();
2666
2667                async fn get_reverse_from(
2668                    graph: &ModuleGraphLayer,
2669                    module: ResolvedVc<Box<dyn Module>>,
2670                ) -> Result<Vec<RcStr>> {
2671                    let mut visited = Vec::new();
2672                    graph.traverse_edges_reverse_dfs(
2673                        vec![module],
2674                        &mut (),
2675                        |_parent, child, _state_| {
2676                            visited.push(child);
2677                            Ok(GraphTraversalAction::Continue)
2678                        },
2679                        |_, _, _| Ok(()),
2680                    )?;
2681                    visited
2682                        .iter()
2683                        .map(|m| m.ident().to_string().owned())
2684                        .try_join()
2685                        .await
2686                }
2687
2688                Ok(ReverseTraversalResults {
2689                    forward,
2690                    reverse_from_d: get_reverse_from(&child_graph, d_module).await?,
2691                    reverse_from_b: get_reverse_from(&child_graph, b_module).await?,
2692                }
2693                .cell())
2694            }
2695
2696            let traversal_results = reverse_traversal_results_operation()
2697                .read_strongly_consistent()
2698                .await?;
2699
2700            assert_eq!(
2701                traversal_results.forward,
2702                vec![
2703                    rcstr!("[test]/a.js"),
2704                    rcstr!("[test]/b.js"),
2705                    rcstr!("[test]/d.js")
2706                ]
2707            );
2708
2709            assert_eq!(
2710                traversal_results.reverse_from_d,
2711                vec![rcstr!("[test]/d.js"), rcstr!("[test]/a.js")]
2712            );
2713
2714            assert_eq!(
2715                traversal_results.reverse_from_b,
2716                vec![rcstr!("[test]/b.js"), rcstr!("[test]/a.js")]
2717            );
2718
2719            Ok(())
2720        })
2721        .await
2722        .unwrap();
2723    }
2724
2725    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2726    async fn test_iter_nodes_modules_through_layered_graph() {
2727        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
2728            BackendOptions::default(),
2729            noop_backing_storage(),
2730        ));
2731        tt.run_once(async move {
2732            #[turbo_tasks::value]
2733            struct Results {
2734                iter_nodes: Vec<RcStr>,
2735                iter_modules: Vec<RcStr>,
2736                iter_nodes_single: Vec<Vec<RcStr>>,
2737                iter_modules_single: Vec<Vec<RcStr>>,
2738            }
2739
2740            #[turbo_tasks::function(operation, root)]
2741            async fn reverse_traversal_results_operation() -> Result<Vc<Results>> {
2742                let fs = VirtualFileSystem::new_with_name(rcstr!("test"));
2743                let root = fs.root().await?;
2744
2745                // a simple linear graph a -> b -> c -> x -> y -> z
2746                // but x -> y -> z is in a parent graph
2747
2748                let repo = TestRepo::new_with_chunking_types(
2749                    &root,
2750                    [
2751                        ("a.js", vec!["b.js"]),
2752                        ("b.js", vec!["c.js"]),
2753                        ("c.js", vec!["x.js"]),
2754                        ("x.js", vec!["y.js", "traced.js"]),
2755                        ("y.js", vec!["z.js"]),
2756                    ],
2757                    [(
2758                        "x.js",
2759                        "traced.js",
2760                        ChunkingType::Traced {
2761                            mode: TracedMode::Entry,
2762                        },
2763                    )],
2764                );
2765                let make_module = |name| {
2766                    Vc::upcast::<Box<dyn Module>>(MockModule::new(root.join(name).unwrap(), repo))
2767                        .to_resolved()
2768                };
2769                let x_module = make_module("x.js").await?;
2770                let a_module = make_module("a.js").await?;
2771
2772                let parent_graph = SingleModuleGraph::new_with_entries(
2773                    GraphEntries::from_chunk_groups(vec![ChunkGroupEntry::Entry {
2774                        modules: vec![x_module],
2775                        heuristics: EntryHeuristics::default(),
2776                    }])
2777                    .resolved_cell(),
2778                    true,
2779                    false,
2780                );
2781
2782                let module_graph = ModuleGraph::from_graphs(
2783                    vec![
2784                        parent_graph,
2785                        SingleModuleGraph::new_with_entries_visited(
2786                            GraphEntries::from_chunk_groups(vec![ChunkGroupEntry::Entry {
2787                                modules: vec![a_module],
2788                                heuristics: EntryHeuristics::default(),
2789                            }])
2790                            .resolved_cell(),
2791                            VisitedModules::from_graph(parent_graph),
2792                            true,
2793                            false,
2794                        ),
2795                    ],
2796                    None,
2797                )
2798                .connect();
2799                let graph_layers = module_graph.iter_graphs().await?;
2800
2801                Ok(Results {
2802                    iter_nodes: module_graph
2803                        .await?
2804                        .iter_reachable_nodes()?
2805                        .map(async |node| {
2806                            Ok(match node {
2807                                SingleModuleGraphNode::Module(module) => {
2808                                    module.ident_string().owned().await?
2809                                }
2810                                SingleModuleGraphNode::VisitedModule { module, .. } => {
2811                                    format!("visited {}", module.ident_string().owned().await?)
2812                                        .into()
2813                                }
2814                            })
2815                        })
2816                        .try_join()
2817                        .await?,
2818                    iter_modules: module_graph
2819                        .await?
2820                        .iter_reachable_modules()?
2821                        .map(|m| m.ident_string().owned())
2822                        .try_join()
2823                        .await?,
2824                    iter_nodes_single: graph_layers
2825                        .iter()
2826                        .map(async |layer| {
2827                            layer
2828                                .connect()
2829                                .await?
2830                                .iter_reachable_nodes()?
2831                                .map(async |node| {
2832                                    Ok(match node {
2833                                        SingleModuleGraphNode::Module(module) => {
2834                                            module.ident_string().owned().await?
2835                                        }
2836                                        SingleModuleGraphNode::VisitedModule { module, .. } => {
2837                                            format!(
2838                                                "visited {}",
2839                                                module.ident_string().owned().await?
2840                                            )
2841                                            .into()
2842                                        }
2843                                    })
2844                                })
2845                                .try_join()
2846                                .await
2847                        })
2848                        .try_join()
2849                        .await?,
2850                    iter_modules_single: graph_layers
2851                        .iter()
2852                        .map(async |layer| {
2853                            layer
2854                                .connect()
2855                                .await?
2856                                .iter_reachable_modules()?
2857                                .map(|m| m.ident_string().owned())
2858                                .try_join()
2859                                .await
2860                        })
2861                        .try_join()
2862                        .await?,
2863                }
2864                .cell())
2865            }
2866
2867            let traversal_results = reverse_traversal_results_operation()
2868                .read_strongly_consistent()
2869                .await?;
2870
2871            assert_eq!(
2872                traversal_results.iter_nodes,
2873                vec![
2874                    rcstr!("[test]/x.js"),
2875                    rcstr!("[test]/a.js"),
2876                    rcstr!("[test]/y.js"),
2877                    rcstr!("[test]/b.js"),
2878                    rcstr!("[test]/z.js"),
2879                    rcstr!("[test]/c.js"),
2880                    rcstr!("visited [test]/x.js")
2881                ]
2882            );
2883            assert_eq!(
2884                traversal_results.iter_modules,
2885                vec![
2886                    rcstr!("[test]/x.js"),
2887                    rcstr!("[test]/a.js"),
2888                    rcstr!("[test]/y.js"),
2889                    rcstr!("[test]/b.js"),
2890                    rcstr!("[test]/z.js"),
2891                    rcstr!("[test]/c.js")
2892                ]
2893            );
2894            assert_eq!(
2895                traversal_results.iter_nodes_single,
2896                vec![
2897                    vec![
2898                        rcstr!("[test]/x.js"),
2899                        rcstr!("[test]/y.js"),
2900                        rcstr!("[test]/z.js")
2901                    ],
2902                    vec![
2903                        rcstr!("[test]/a.js"),
2904                        rcstr!("[test]/b.js"),
2905                        rcstr!("[test]/c.js"),
2906                        rcstr!("visited [test]/x.js")
2907                    ]
2908                ]
2909            );
2910            assert_eq!(
2911                traversal_results.iter_modules_single,
2912                vec![
2913                    vec![
2914                        rcstr!("[test]/x.js"),
2915                        rcstr!("[test]/y.js"),
2916                        rcstr!("[test]/z.js")
2917                    ],
2918                    vec![
2919                        rcstr!("[test]/a.js"),
2920                        rcstr!("[test]/b.js"),
2921                        rcstr!("[test]/c.js")
2922                    ]
2923                ]
2924            );
2925
2926            Ok(())
2927        })
2928        .await
2929        .unwrap();
2930    }
2931
2932    #[turbo_tasks::value(shared)]
2933    struct TestRepo {
2934        repo: FxHashMap<FileSystemPath, Vec<FileSystemPath>>,
2935        chunking_types: FxHashMap<(FileSystemPath, FileSystemPath), ChunkingType>,
2936    }
2937
2938    impl TestRepo {
2939        fn new(
2940            root: &FileSystemPath,
2941            dependencies: impl IntoIterator<Item = (impl AsRef<str>, Vec<impl AsRef<str>>)>,
2942        ) -> Vc<Self> {
2943            Self::new_with_chunking_types(
2944                root,
2945                dependencies,
2946                std::iter::empty::<(RcStr, RcStr, ChunkingType)>(),
2947            )
2948        }
2949
2950        fn new_with_chunking_types(
2951            root: &FileSystemPath,
2952            dependencies: impl IntoIterator<Item = (impl AsRef<str>, Vec<impl AsRef<str>>)>,
2953            chunking_types: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>, ChunkingType)>,
2954        ) -> Vc<Self> {
2955            let chunking_types = chunking_types
2956                .into_iter()
2957                .map(|(from, to, ty)| {
2958                    (
2959                        (
2960                            root.join(from.as_ref()).unwrap(),
2961                            root.join(to.as_ref()).unwrap(),
2962                        ),
2963                        ty,
2964                    )
2965                })
2966                .collect::<FxHashMap<_, _>>();
2967            Self {
2968                repo: dependencies
2969                    .into_iter()
2970                    .map(|(k, v)| {
2971                        (
2972                            root.join(k.as_ref()).unwrap(),
2973                            v.iter().map(|f| root.join(f.as_ref()).unwrap()).collect(),
2974                        )
2975                    })
2976                    .collect(),
2977                chunking_types,
2978            }
2979            .cell()
2980        }
2981    }
2982
2983    #[turbo_tasks::value]
2984    struct MockModule {
2985        path: FileSystemPath,
2986        repo: ResolvedVc<TestRepo>,
2987    }
2988    #[turbo_tasks::value_impl]
2989    impl MockModule {
2990        #[turbo_tasks::function]
2991        fn new(path: FileSystemPath, repo: ResolvedVc<TestRepo>) -> Vc<Self> {
2992            Self { path, repo }.cell()
2993        }
2994    }
2995
2996    #[turbo_tasks::value_impl]
2997    impl Asset for MockModule {
2998        #[turbo_tasks::function]
2999        fn content(&self) -> Vc<AssetContent> {
3000            panic!("MockModule::content shouldn't be called")
3001        }
3002    }
3003
3004    #[turbo_tasks::value_impl]
3005    impl Module for MockModule {
3006        #[turbo_tasks::function]
3007        fn ident(&self) -> Vc<AssetIdent> {
3008            AssetIdent::from_path(self.path.clone()).into_vc()
3009        }
3010
3011        #[turbo_tasks::function]
3012        fn source(&self) -> Vc<crate::source::OptionSource> {
3013            Vc::cell(None)
3014        }
3015
3016        #[turbo_tasks::function]
3017        async fn references(&self) -> Result<Vc<ModuleReferences>> {
3018            let repo = self.repo.await?;
3019            let references = match repo.repo.get(&self.path) {
3020                Some(deps) => {
3021                    deps.iter()
3022                        .map(async |p| {
3023                            Vc::upcast::<Box<dyn ModuleReference>>(MockModuleReference::new(
3024                                ResolvedVc::upcast(
3025                                    MockModule::new(p.clone(), *self.repo).to_resolved().await?,
3026                                ),
3027                                rcstr!("normal-dep"),
3028                                repo.chunking_types
3029                                    .get(&(self.path.clone(), p.clone()))
3030                                    .cloned()
3031                                    .unwrap_or(ChunkingType::Parallel {
3032                                        inherit_async: true,
3033                                        hoisted: false,
3034                                    }),
3035                            ))
3036                            .to_resolved()
3037                            .await
3038                        })
3039                        .try_join()
3040                        .await?
3041                }
3042                None => vec![],
3043            };
3044
3045            Ok(Vc::cell(references))
3046        }
3047        #[turbo_tasks::function]
3048        fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
3049            ModuleSideEffects::SideEffectful.cell()
3050        }
3051    }
3052
3053    #[turbo_tasks::value]
3054    #[derive(ValueToString)]
3055    #[value_to_string(self.description)]
3056    struct MockModuleReference {
3057        asset: ResolvedVc<Box<dyn Module>>,
3058        description: RcStr,
3059        chunking_type: ChunkingType,
3060    }
3061
3062    impl MockModuleReference {
3063        pub fn new(
3064            asset: ResolvedVc<Box<dyn Module>>,
3065            description: RcStr,
3066            chunking_type: ChunkingType,
3067        ) -> Vc<Self> {
3068            MockModuleReference {
3069                asset,
3070                description,
3071                chunking_type,
3072            }
3073            .cell()
3074        }
3075    }
3076
3077    #[turbo_tasks::value_impl]
3078    impl ModuleReference for MockModuleReference {
3079        #[turbo_tasks::function]
3080        fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
3081            *ModuleResolveResult::module(self.asset)
3082        }
3083
3084        fn chunking_type(&self) -> Option<ChunkingType> {
3085            Some(self.chunking_type.clone())
3086        }
3087    }
3088
3089    /// Constructs a graph based on the provided dependency adjacency lists and calls the given test
3090    /// function.
3091    ///
3092    /// # Parameters
3093    /// - `entries`: A vector of entry module names (as `RcStr`). These are the starting points for
3094    ///   the graph.
3095    /// - `graph`: A map from module name (`RcStr`) to a vector of its dependency module names
3096    ///   (`RcStr`). Represents the adjacency list of the graph.
3097    /// - `test_fn`: A function that is called with:
3098    ///     - `ReadRef<SingleModuleGraph>`: The constructed module graph.
3099    ///     - `Vec<ResolvedVc<Box<dyn Module>>>`: The resolved entry modules.
3100    ///     - `FxHashMap<ResolvedVc<Box<dyn Module>>, RcStr>`: A mapping from module to its name for
3101    ///       easier analysis in tests.
3102    async fn run_graph_test(
3103        entries: Vec<RcStr>,
3104        graph: FxHashMap<RcStr, Vec<RcStr>>,
3105        test_fn: impl FnOnce(
3106            &ModuleGraph,
3107            Vec<ResolvedVc<Box<dyn Module>>>,
3108            FxHashMap<ResolvedVc<Box<dyn Module>>, RcStr>,
3109        ) -> Result<()>
3110        + Send
3111        + 'static,
3112    ) {
3113        #[turbo_tasks::value(serialization = "skip", eq = "manual", cell = "new")]
3114        struct SetupGraph {
3115            module_graph: ReadRef<ModuleGraph>,
3116            entry_modules: Vec<ResolvedVc<Box<dyn Module>>>,
3117            module_to_name: FxHashMap<ResolvedVc<Box<dyn Module>>, RcStr>,
3118        }
3119
3120        #[turbo_tasks::function(operation, root)]
3121        async fn setup_graph(
3122            entries: Vec<RcStr>,
3123            graph_entries: Vec<(RcStr, Vec<RcStr>)>,
3124        ) -> Result<Vc<SetupGraph>> {
3125            let fs = VirtualFileSystem::new_with_name(rcstr!("test"));
3126            let root = fs.root().await?;
3127
3128            let repo = TestRepo::new(&root, graph_entries);
3129            let entry_modules = entries
3130                .iter()
3131                .map(|e| {
3132                    Vc::upcast::<Box<dyn Module>>(MockModule::new(root.join(e).unwrap(), repo))
3133                        .to_resolved()
3134                })
3135                .try_join()
3136                .await?;
3137            let graph = SingleModuleGraph::new_with_entries(
3138                GraphEntries::resolved_cell(GraphEntries::new(
3139                    vec![ChunkGroupEntry::Entry {
3140                        modules: entry_modules.clone(),
3141                        heuristics: EntryHeuristics::default(),
3142                    }],
3143                    vec![],
3144                )),
3145                false,
3146                false,
3147            );
3148
3149            // Create a simple name mapping to make analyzing the visitors easier.
3150            // Technically they could always pull this name off of the
3151            // `module.ident().await?.path.path` themselves but you cannot `await` in visitors.
3152            let module_to_name = graph
3153                .connect()
3154                .await?
3155                .modules
3156                .keys()
3157                .map(async |m| Ok((*m, m.ident().await?.path.path.clone())))
3158                .try_join()
3159                .await?
3160                .into_iter()
3161                .collect();
3162            let module_graph = ModuleGraph::from_graphs(vec![graph], None)
3163                .connect()
3164                .await?;
3165
3166            Ok(SetupGraph {
3167                module_graph,
3168                entry_modules,
3169                module_to_name,
3170            }
3171            .cell())
3172        }
3173
3174        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3175            BackendOptions::default(),
3176            noop_backing_storage(),
3177        ));
3178        let graph_entries = graph.into_iter().collect::<Vec<_>>();
3179        tt.run_once(async move {
3180            let setup = setup_graph(entries, graph_entries)
3181                .read_strongly_consistent()
3182                .await?;
3183
3184            test_fn(
3185                &setup.module_graph,
3186                setup.entry_modules.clone(),
3187                setup.module_to_name.clone(),
3188            )
3189        })
3190        .await
3191        .unwrap();
3192    }
3193
3194    /// Asserts that the ranks are a permutation of `0..n` and that every (non back-) edge in
3195    /// `edges` goes from a lower to a higher rank.
3196    fn assert_ranks(
3197        ranks: &FxHashMap<ResolvedVc<Box<dyn Module>>, usize>,
3198        module_to_name: &FxHashMap<ResolvedVc<Box<dyn Module>>, RcStr>,
3199        edges: &[(&str, &str)],
3200    ) -> FxHashMap<RcStr, usize> {
3201        let by_name: FxHashMap<RcStr, usize> = ranks
3202            .iter()
3203            .map(|(m, r)| (module_to_name[m].clone(), *r))
3204            .collect();
3205        assert_eq!(by_name.len(), ranks.len(), "modules must be ranked once");
3206        let mut sorted = ranks.values().copied().collect::<Vec<_>>();
3207        sorted.sort();
3208        assert_eq!(sorted, (0..ranks.len()).collect::<Vec<_>>());
3209        for (from, to) in edges {
3210            assert!(
3211                by_name[*from] < by_name[*to],
3212                "expected rank({from}) < rank({to}), ranks: {by_name:?}"
3213            );
3214        }
3215        by_name
3216    }
3217
3218    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3219    async fn test_reverse_postorder_ranks_diamond() {
3220        run_graph_test(
3221            vec![rcstr!("a.js")],
3222            {
3223                // d is reachable via paths of length 1, 2 and 3
3224                let mut deps = FxHashMap::default();
3225                deps.insert(
3226                    rcstr!("a.js"),
3227                    vec![rcstr!("b.js"), rcstr!("d.js"), rcstr!("e.js")],
3228                );
3229                deps.insert(rcstr!("b.js"), vec![rcstr!("c.js")]);
3230                deps.insert(rcstr!("c.js"), vec![rcstr!("d.js")]);
3231                deps.insert(rcstr!("e.js"), vec![rcstr!("d.js")]);
3232                deps
3233            },
3234            |graph, entry_modules, module_to_name| {
3235                let ranks = graph.reverse_postorder_ranks(entry_modules.clone())?;
3236                assert_eq!(ranks.len(), 5);
3237                assert_ranks(
3238                    &ranks,
3239                    &module_to_name,
3240                    &[
3241                        ("a.js", "b.js"),
3242                        ("a.js", "d.js"),
3243                        ("a.js", "e.js"),
3244                        ("b.js", "c.js"),
3245                        ("c.js", "d.js"),
3246                        ("e.js", "d.js"),
3247                    ],
3248                );
3249
3250                // Using the ranks as priority, every module is visited exactly once, even if the
3251                // visitor always continues.
3252                let visit_count = graph.traverse_edges_fixed_point_with_priority(
3253                    entry_modules
3254                        .iter()
3255                        .map(|m| (*m, std::cmp::Reverse(ranks[m])))
3256                        .collect::<Vec<_>>(),
3257                    &mut (),
3258                    |_, _, _, _| Ok(GraphTraversalAction::Continue),
3259                    |m, _| Ok(std::cmp::Reverse(ranks[&m])),
3260                )?;
3261                assert_eq!(visit_count, 5);
3262                Ok(())
3263            },
3264        )
3265        .await;
3266    }
3267
3268    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3269    async fn test_reverse_postorder_ranks_cycle() {
3270        run_graph_test(
3271            vec![rcstr!("a.js")],
3272            {
3273                let mut deps = FxHashMap::default();
3274                deps.insert(rcstr!("a.js"), vec![rcstr!("b.js")]);
3275                deps.insert(rcstr!("b.js"), vec![rcstr!("c.js")]);
3276                deps.insert(rcstr!("c.js"), vec![rcstr!("a.js"), rcstr!("d.js")]);
3277                deps
3278            },
3279            |graph, entry_modules, module_to_name| {
3280                let ranks = graph.reverse_postorder_ranks(entry_modules)?;
3281                assert_eq!(ranks.len(), 4);
3282                // c -> a is the back edge
3283                assert_ranks(
3284                    &ranks,
3285                    &module_to_name,
3286                    &[("a.js", "b.js"), ("b.js", "c.js"), ("c.js", "d.js")],
3287                );
3288                Ok(())
3289            },
3290        )
3291        .await;
3292    }
3293
3294    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3295    async fn test_reverse_postorder_ranks_multiple_entries() {
3296        run_graph_test(
3297            vec![rcstr!("a.js"), rcstr!("b.js")],
3298            {
3299                let mut deps = FxHashMap::default();
3300                deps.insert(rcstr!("a.js"), vec![rcstr!("b.js")]);
3301                deps.insert(rcstr!("b.js"), vec![rcstr!("c.js")]);
3302                deps
3303            },
3304            |graph, entry_modules, module_to_name| {
3305                let [a, b] = entry_modules[..] else {
3306                    panic!("expected two entries");
3307                };
3308                // The importing entry `a` must be ranked before `b`, independent of the order of
3309                // the entries, and duplicate entries are ignored.
3310                for entries in [vec![a, b], vec![b, a], vec![b, a, b, a]] {
3311                    let ranks = graph.reverse_postorder_ranks(entries)?;
3312                    assert_eq!(ranks.len(), 3);
3313                    assert_ranks(
3314                        &ranks,
3315                        &module_to_name,
3316                        &[("a.js", "b.js"), ("b.js", "c.js")],
3317                    );
3318                }
3319                Ok(())
3320            },
3321        )
3322        .await;
3323    }
3324
3325    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3326    async fn test_reverse_postorder_ranks_through_layered_graph() {
3327        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3328            BackendOptions::default(),
3329            noop_backing_storage(),
3330        ));
3331        tt.run_once(async move {
3332            #[turbo_tasks::value]
3333            struct Results {
3334                ranks_from_child_entry: Vec<(RcStr, usize)>,
3335                ranks_from_all_entries: Vec<(RcStr, usize)>,
3336            }
3337
3338            #[turbo_tasks::function(operation, root)]
3339            async fn layered_ranks_operation() -> Result<Vc<Results>> {
3340                let fs = VirtualFileSystem::new_with_name(rcstr!("test"));
3341                let root = fs.root().await?;
3342
3343                // a -> b -> c -> x -> y -> z, but x -> y -> z is in a parent graph, so the child
3344                // graph only contains a `VisitedModule` node for x
3345                let repo = TestRepo::new(
3346                    &root,
3347                    [
3348                        ("a.js", vec!["b.js"]),
3349                        ("b.js", vec!["c.js"]),
3350                        ("c.js", vec!["x.js"]),
3351                        ("x.js", vec!["y.js"]),
3352                        ("y.js", vec!["z.js"]),
3353                    ],
3354                );
3355                let make_module = |name| {
3356                    Vc::upcast::<Box<dyn Module>>(MockModule::new(root.join(name).unwrap(), repo))
3357                        .to_resolved()
3358                };
3359                let x_module = make_module("x.js").await?;
3360                let a_module = make_module("a.js").await?;
3361
3362                let parent_graph = SingleModuleGraph::new_with_entries(
3363                    GraphEntries::from_chunk_groups(vec![ChunkGroupEntry::Entry {
3364                        modules: vec![x_module],
3365                        heuristics: EntryHeuristics::default(),
3366                    }])
3367                    .resolved_cell(),
3368                    false,
3369                    false,
3370                );
3371                let module_graph = ModuleGraph::from_graphs(
3372                    vec![
3373                        parent_graph,
3374                        SingleModuleGraph::new_with_entries_visited(
3375                            GraphEntries::from_chunk_groups(vec![ChunkGroupEntry::Entry {
3376                                modules: vec![a_module],
3377                                heuristics: EntryHeuristics::default(),
3378                            }])
3379                            .resolved_cell(),
3380                            VisitedModules::from_graph(parent_graph),
3381                            false,
3382                            false,
3383                        ),
3384                    ],
3385                    None,
3386                )
3387                .connect()
3388                .await?;
3389
3390                async fn named(
3391                    ranks: FxHashMap<ResolvedVc<Box<dyn Module>>, usize>,
3392                ) -> Result<Vec<(RcStr, usize)>> {
3393                    let mut ranks = ranks
3394                        .into_iter()
3395                        .map(async |(m, r)| Ok((m.ident().await?.path.path.clone(), r)))
3396                        .try_join()
3397                        .await?;
3398                    ranks.sort_by_key(|(_, r)| *r);
3399                    Ok(ranks)
3400                }
3401
3402                Ok(Results {
3403                    ranks_from_child_entry: named(
3404                        module_graph.reverse_postorder_ranks([a_module])?,
3405                    )
3406                    .await?,
3407                    ranks_from_all_entries: named(
3408                        module_graph.reverse_postorder_ranks([x_module, a_module])?,
3409                    )
3410                    .await?,
3411                }
3412                .cell())
3413            }
3414
3415            let results = layered_ranks_operation().read_strongly_consistent().await?;
3416
3417            // Every module is ranked exactly once, with the `VisitedModule` x resolved to the
3418            // module in the parent graph, and the ranks follow the edges across the graphs.
3419            let expected = ["a.js", "b.js", "c.js", "x.js", "y.js", "z.js"]
3420                .into_iter()
3421                .enumerate()
3422                .map(|(i, name)| (RcStr::from(name), i))
3423                .collect::<Vec<_>>();
3424            assert_eq!(results.ranks_from_child_entry, expected);
3425            assert_eq!(results.ranks_from_all_entries, expected);
3426
3427            Ok(())
3428        })
3429        .await
3430        .unwrap();
3431    }
3432
3433    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3434    async fn test_traverse_edges_fixed_point_dedups_entries() {
3435        run_graph_test(
3436            vec![rcstr!("a.js"), rcstr!("b.js")],
3437            {
3438                let mut deps = FxHashMap::default();
3439                deps.insert(rcstr!("a.js"), vec![rcstr!("b.js")]);
3440                deps
3441            },
3442            |graph, entry_modules, _| {
3443                let [a, b] = entry_modules[..] else {
3444                    panic!("expected two entries");
3445                };
3446                let mut root_visits = 0;
3447                // `b` is an entry and reachable from the entry `a`, and `a` is passed twice. `a`
3448                // is visited first, its reference to the already queued entry `b` must not queue
3449                // `b` a second time.
3450                let visit_count = graph.traverse_edges_fixed_point_with_priority(
3451                    [(a, 1), (b, 0), (a, 1)],
3452                    &mut (),
3453                    |parent, _, _, _| {
3454                        if parent.is_none() {
3455                            root_visits += 1;
3456                        }
3457                        Ok(GraphTraversalAction::Continue)
3458                    },
3459                    |_, _| Ok(0),
3460                )?;
3461                assert_eq!(root_visits, 2);
3462                assert_eq!(visit_count, 2);
3463                Ok(())
3464            },
3465        )
3466        .await;
3467    }
3468
3469    #[turbo_tasks::value(serialization = "skip", eq = "manual", cell = "new")]
3470    struct ChunkGroupInfoTestGraph {
3471        module_graph: ReadRef<ModuleGraph>,
3472        module_to_name: FxHashMap<ResolvedVc<Box<dyn Module>>, RcStr>,
3473    }
3474
3475    /// Two entries with heuristics, a diamond with paths of different lengths into a shared
3476    /// module, an async reference and an isolated reference with a merge tag:
3477    ///
3478    /// ```text
3479    /// page1 (clusters [0])                 page2 (clusters [1], high priority)
3480    ///  |-> a1 -> a2 -> shared <-------------|
3481    ///  |-> shared -> lib                    |-isolated(merge_tag "m")-> iso -> lib
3482    ///  |-async-> lazy -> lib
3483    /// ```
3484    #[turbo_tasks::function(operation, root)]
3485    async fn chunk_group_info_test_graph() -> Result<Vc<ChunkGroupInfoTestGraph>> {
3486        let fs = VirtualFileSystem::new_with_name(rcstr!("test"));
3487        let root = fs.root().await?;
3488        let repo = TestRepo::new_with_chunking_types(
3489            &root,
3490            [
3491                ("page1.js", vec!["a1.js", "shared.js", "lazy.js"]),
3492                ("a1.js", vec!["a2.js"]),
3493                ("a2.js", vec!["shared.js"]),
3494                ("page2.js", vec!["shared.js", "iso.js"]),
3495                ("shared.js", vec!["lib.js"]),
3496                ("lazy.js", vec!["lib.js"]),
3497                ("iso.js", vec!["lib.js"]),
3498            ],
3499            [
3500                ("page1.js", "lazy.js", ChunkingType::Async),
3501                (
3502                    "page2.js",
3503                    "iso.js",
3504                    ChunkingType::Isolated {
3505                        _ty: crate::chunk::ChunkGroupType::Entry,
3506                        merge_tag: Some(rcstr!("m")),
3507                    },
3508                ),
3509            ],
3510        );
3511        let module = |name: &str| {
3512            Vc::upcast::<Box<dyn Module>>(MockModule::new(root.join(name).unwrap(), repo))
3513                .to_resolved()
3514        };
3515        let entries = vec![
3516            ChunkGroupEntry::Entry {
3517                modules: vec![module("page1.js").await?],
3518                heuristics: EntryHeuristics {
3519                    clusters: vec![0],
3520                    high_priority: false,
3521                },
3522            },
3523            ChunkGroupEntry::Entry {
3524                modules: vec![module("page2.js").await?],
3525                heuristics: EntryHeuristics {
3526                    clusters: vec![1],
3527                    high_priority: true,
3528                },
3529            },
3530        ];
3531        let graph = SingleModuleGraph::new_with_entries(
3532            GraphEntries::resolved_cell(GraphEntries::new(entries, vec![])),
3533            false,
3534            false,
3535        );
3536        let module_to_name = graph
3537            .connect()
3538            .await?
3539            .modules
3540            .keys()
3541            .map(async |m| Ok((*m, m.ident().await?.path.path.clone())))
3542            .try_join()
3543            .await?
3544            .into_iter()
3545            .collect();
3546        let module_graph = ModuleGraph::from_graphs(vec![graph], None)
3547            .connect()
3548            .await?;
3549        Ok(ChunkGroupInfoTestGraph {
3550            module_graph,
3551            module_to_name,
3552        }
3553        .cell())
3554    }
3555
3556    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3557    async fn test_chunk_group_info_semantics() {
3558        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
3559            BackendOptions::default(),
3560            noop_backing_storage(),
3561        ));
3562        tt.run_once(async move {
3563            let setup = chunk_group_info_test_graph()
3564                .read_strongly_consistent()
3565                .await?;
3566            let names = &setup.module_to_name;
3567            let name = |m: &ResolvedVc<Box<dyn Module>>| names[m].to_string();
3568            let info = compute_chunk_group_info(&setup.module_graph).await?.await?;
3569
3570            // Describe chunk groups by name, independent of their index.
3571            fn describe(
3572                info: &ChunkGroupInfo,
3573                index: usize,
3574                names: &FxHashMap<ResolvedVc<Box<dyn Module>>, RcStr>,
3575            ) -> String {
3576                let list = |modules: &[ResolvedVc<Box<dyn Module>>]| {
3577                    modules
3578                        .iter()
3579                        .map(|m| names[m].to_string())
3580                        .collect::<Vec<_>>()
3581                        .join(",")
3582                };
3583                match info.chunk_groups.get_index(index).unwrap() {
3584                    ChunkGroup::Entry(modules) => format!("entry {}", list(modules)),
3585                    ChunkGroup::Async(module) => format!("async {}", names[module]),
3586                    ChunkGroup::IsolatedMerged {
3587                        parent,
3588                        merge_tag,
3589                        entries,
3590                    } => format!(
3591                        "isolated merged {merge_tag} [{}] of ({})",
3592                        list(entries),
3593                        describe(info, *parent, names)
3594                    ),
3595                    other => panic!("unexpected chunk group {other:?}"),
3596                }
3597            }
3598
3599            let mut membership = info
3600                .module_chunk_groups
3601                .await?
3602                .iter()
3603                .map(|(module, groups)| {
3604                    let mut groups = groups
3605                        .iter()
3606                        .map(|i| describe(&info, i as usize, names))
3607                        .collect::<Vec<_>>();
3608                    groups.sort();
3609                    (name(module), groups)
3610                })
3611                .collect::<Vec<_>>();
3612            membership.sort();
3613
3614            let page1 = "entry page1.js";
3615            let page2 = "entry page2.js";
3616            let lazy = "async lazy.js";
3617            let iso = "isolated merged m [iso.js] of (entry page2.js)";
3618            let expected = |groups: &[&str]| {
3619                let mut groups = groups.iter().map(|s| s.to_string()).collect::<Vec<_>>();
3620                groups.sort();
3621                groups
3622            };
3623            assert_eq!(
3624                membership,
3625                vec![
3626                    ("a1.js".to_string(), expected(&[page1])),
3627                    ("a2.js".to_string(), expected(&[page1])),
3628                    ("iso.js".to_string(), expected(&[iso])),
3629                    ("lazy.js".to_string(), expected(&[lazy])),
3630                    ("lib.js".to_string(), expected(&[page1, page2, lazy, iso])),
3631                    ("page1.js".to_string(), expected(&[page1])),
3632                    ("page2.js".to_string(), expected(&[page2])),
3633                    ("shared.js".to_string(), expected(&[page1, page2])),
3634                ]
3635            );
3636
3637            // Heuristics: async groups inherit from the groups of the referencing module, merged
3638            // groups from their parent group.
3639            let mut heuristics = (0..info.chunk_groups.len())
3640                .map(|i| {
3641                    (
3642                        describe(&info, i, names),
3643                        info.chunking_heuristics.clusters[i].clone(),
3644                        info.chunking_heuristics.priority_routes.contains(i as u32),
3645                    )
3646                })
3647                .collect::<Vec<_>>();
3648            heuristics.sort();
3649            assert_eq!(
3650                heuristics,
3651                vec![
3652                    (lazy.to_string(), vec![0], false),
3653                    (page1.to_string(), vec![0], false),
3654                    (page2.to_string(), vec![1], true),
3655                    (iso.to_string(), vec![1], true),
3656                ]
3657            );
3658
3659            Ok(())
3660        })
3661        .await
3662        .unwrap();
3663    }
3664}