turbopack_core/module_graph/
mod.rs

1use core::panic;
2use std::{
3    collections::{BinaryHeap, VecDeque, hash_map::Entry},
4    future::Future,
5};
6
7use anyhow::{Context, Result, bail};
8use auto_hash_map::AutoSet;
9use petgraph::{
10    graph::{DiGraph, EdgeIndex, NodeIndex},
11    visit::{EdgeRef, IntoNeighbors, IntoNodeReferences, NodeIndexable, Reversed},
12};
13use rustc_hash::{FxHashMap, FxHashSet};
14use serde::{Deserialize, Serialize};
15use tracing::{Instrument, Level, Span};
16use turbo_rcstr::RcStr;
17use turbo_tasks::{
18    CollectiblesSource, FxIndexMap, NonLocalValue, ReadRef, ResolvedVc, TryJoinIterExt,
19    ValueToString, Vc,
20    debug::ValueDebugFormat,
21    graph::{AdjacencyMap, GraphTraversal, Visit, VisitControlFlow},
22    trace::TraceRawVcs,
23};
24use turbo_tasks_fs::FileSystemPath;
25
26use crate::{
27    chunk::{AsyncModuleInfo, ChunkingContext, ChunkingType},
28    issue::{ImportTrace, ImportTracer, ImportTraces, Issue},
29    module::Module,
30    module_graph::{
31        async_module_info::{AsyncModulesInfo, compute_async_module_info},
32        chunk_group_info::{ChunkGroupEntry, ChunkGroupInfo, compute_chunk_group_info},
33        merged_modules::{MergedModuleInfo, compute_merged_modules},
34        module_batches::{ModuleBatchesGraph, compute_module_batches},
35        style_groups::{StyleGroups, StyleGroupsConfig, compute_style_groups},
36        traced_di_graph::TracedDiGraph,
37    },
38    reference::primary_chunkable_referenced_modules,
39    resolve::ExportUsage,
40};
41
42pub mod async_module_info;
43pub mod chunk_group_info;
44pub mod export_usage;
45pub mod merged_modules;
46pub mod module_batch;
47pub(crate) mod module_batches;
48pub(crate) mod style_groups;
49mod traced_di_graph;
50
51pub use self::module_batches::BatchingConfig;
52
53#[derive(
54    Debug, Copy, Clone, Eq, PartialOrd, Ord, Hash, PartialEq, Serialize, Deserialize, TraceRawVcs,
55)]
56pub struct GraphNodeIndex {
57    #[turbo_tasks(trace_ignore)]
58    graph_idx: u32,
59    #[turbo_tasks(trace_ignore)]
60    node_idx: NodeIndex,
61}
62impl GraphNodeIndex {
63    #[inline(always)]
64    fn graph_idx(&self) -> usize {
65        self.graph_idx as usize
66    }
67}
68
69unsafe impl NonLocalValue for GraphNodeIndex {}
70
71#[turbo_tasks::value]
72#[derive(Clone, Debug)]
73pub struct VisitedModules {
74    pub modules: FxIndexMap<ResolvedVc<Box<dyn Module>>, GraphNodeIndex>,
75    next_graph_idx: u32,
76}
77
78#[turbo_tasks::value_impl]
79impl VisitedModules {
80    #[turbo_tasks::function]
81    pub fn empty() -> Vc<Self> {
82        Self {
83            modules: Default::default(),
84            next_graph_idx: 0,
85        }
86        .cell()
87    }
88
89    #[turbo_tasks::function]
90    pub async fn from_graph(graph: Vc<SingleModuleGraph>) -> Result<Vc<Self>> {
91        Ok(Self {
92            modules: graph
93                .await?
94                .enumerate_nodes()
95                .flat_map(|(node_idx, module)| match module {
96                    SingleModuleGraphNode::Module(module) => Some((
97                        *module,
98                        GraphNodeIndex {
99                            graph_idx: 0,
100                            node_idx,
101                        },
102                    )),
103                    SingleModuleGraphNode::VisitedModule { .. } => None,
104                })
105                .collect(),
106            next_graph_idx: 1,
107        }
108        .cell())
109    }
110
111    #[turbo_tasks::function]
112    pub fn with_incremented_index(&self) -> Result<Vc<Self>> {
113        Ok(Self {
114            modules: self.modules.clone(),
115            next_graph_idx: self.next_graph_idx + 1,
116        }
117        .cell())
118    }
119
120    #[turbo_tasks::function]
121    pub async fn concatenate(&self, graph: Vc<SingleModuleGraph>) -> Result<Vc<Self>> {
122        let graph = graph.await?;
123        let iter = self
124            .modules
125            .iter()
126            .map(|(module, idx)| (*module, *idx))
127            .chain(
128                graph
129                    .enumerate_nodes()
130                    .flat_map(|(node_idx, module)| match module {
131                        SingleModuleGraphNode::Module(module) => Some((
132                            *module,
133                            GraphNodeIndex {
134                                graph_idx: self.next_graph_idx,
135                                node_idx,
136                            },
137                        )),
138                        SingleModuleGraphNode::VisitedModule { .. } => None,
139                    }),
140            );
141
142        let mut map = FxIndexMap::with_capacity_and_hasher(
143            self.modules.len() + graph.number_of_modules,
144            Default::default(),
145        );
146        for (k, v) in iter {
147            map.entry(k).or_insert(v);
148        }
149        map.shrink_to_fit();
150
151        Ok(Self {
152            modules: map,
153            next_graph_idx: self.next_graph_idx + 1,
154        }
155        .cell())
156    }
157}
158
159pub type GraphEntriesT = Vec<ChunkGroupEntry>;
160
161#[turbo_tasks::value(transparent)]
162pub struct GraphEntries(GraphEntriesT);
163
164#[turbo_tasks::value_impl]
165impl GraphEntries {
166    #[turbo_tasks::function]
167    pub fn empty() -> Vc<Self> {
168        Vc::cell(Vec::new())
169    }
170}
171
172#[turbo_tasks::value(cell = "new", eq = "manual")]
173#[derive(Clone, Default)]
174pub struct SingleModuleGraph {
175    pub graph: TracedDiGraph<SingleModuleGraphNode, RefData>,
176
177    /// The number of modules in the graph (excluding VisitedModule nodes)
178    pub number_of_modules: usize,
179
180    // NodeIndex isn't necessarily stable (because of swap_remove), but we never remove nodes.
181    //
182    // HashMaps have nondeterministic order, but this map is only used for lookups (in
183    // `get_module`) and not iteration.
184    //
185    // This contains Vcs, but they are already contained in the graph, so no need to trace this.
186    #[turbo_tasks(trace_ignore)]
187    modules: FxHashMap<ResolvedVc<Box<dyn Module>>, NodeIndex>,
188
189    #[turbo_tasks(trace_ignore)]
190    pub entries: GraphEntriesT,
191}
192
193#[derive(
194    Debug,
195    Clone,
196    Hash,
197    TraceRawVcs,
198    Serialize,
199    Deserialize,
200    Eq,
201    PartialEq,
202    ValueDebugFormat,
203    NonLocalValue,
204)]
205pub struct RefData {
206    pub chunking_type: ChunkingType,
207    pub export: ExportUsage,
208}
209
210impl SingleModuleGraph {
211    /// Walks the graph starting from the given entries and collects all reachable nodes, skipping
212    /// nodes listed in `visited_modules`
213    /// The resulting graph's outgoing edges are in reverse order.
214    async fn new_inner(
215        entries: &GraphEntriesT,
216        visited_modules: &FxIndexMap<ResolvedVc<Box<dyn Module>>, GraphNodeIndex>,
217        include_traced: bool,
218    ) -> Result<Vc<Self>> {
219        let emit_spans = tracing::enabled!(Level::INFO);
220        let root_edges = entries
221            .iter()
222            .flat_map(|e| e.entries())
223            .map(|e| async move {
224                Ok(SingleModuleGraphBuilderEdge {
225                    to: SingleModuleGraphBuilderNode::new_module(emit_spans, e).await?,
226                    export: ExportUsage::All,
227                })
228            })
229            .try_join()
230            .await?;
231
232        let (children_nodes_iter, visited_nodes) = AdjacencyMap::new()
233            .skip_duplicates_with_key(|node: &(SingleModuleGraphBuilderNode, ExportUsage)| &node.0)
234            .visit(
235                root_edges,
236                SingleModuleGraphBuilder {
237                    visited_modules,
238                    emit_spans,
239                    include_traced,
240                },
241            )
242            .await
243            .completed()?
244            .into_inner_with_visited();
245        let node_count = visited_nodes.0.len();
246        drop(visited_nodes);
247
248        let mut graph: DiGraph<SingleModuleGraphNode, RefData> = DiGraph::with_capacity(
249            node_count,
250            // From real world measurements each module has about 3-4 children
251            // If it has more this would cause an additional allocation, but that's fine
252            node_count * 4,
253        );
254
255        let mut number_of_modules = 0;
256        let mut modules: FxHashMap<ResolvedVc<Box<dyn Module>>, NodeIndex> =
257            FxHashMap::with_capacity_and_hasher(node_count, Default::default());
258        {
259            let _span = tracing::info_span!("build module graph").entered();
260            for (parent, (current, export)) in children_nodes_iter.into_breadth_first_edges() {
261                let parent_edge = match parent.map(|v| v.0) {
262                    Some(SingleModuleGraphBuilderNode::Module { module, .. }) => Some((
263                        *modules.get(&module).unwrap(),
264                        RefData {
265                            chunking_type: COMMON_CHUNKING_TYPE,
266                            export,
267                        },
268                    )),
269                    Some(SingleModuleGraphBuilderNode::ChunkableReference { .. }) => {
270                        // Handled when visiting ChunkableReference below
271                        continue;
272                    }
273                    Some(SingleModuleGraphBuilderNode::VisitedModule { .. }) => unreachable!(),
274                    None => None,
275                };
276
277                match current {
278                    SingleModuleGraphBuilderNode::Module { module, ident: _ } => {
279                        // Find the current node, if it was already added
280                        let current_idx = if let Some(current_idx) = modules.get(&module) {
281                            *current_idx
282                        } else {
283                            let idx = graph.add_node(SingleModuleGraphNode::Module(module));
284                            number_of_modules += 1;
285                            modules.insert(module, idx);
286                            idx
287                        };
288                        // Add the edge
289                        if let Some((parent_idx, ref_data)) = parent_edge {
290                            graph.add_edge(parent_idx, current_idx, ref_data);
291                        }
292                    }
293                    SingleModuleGraphBuilderNode::VisitedModule { module, idx } => {
294                        // Find the current node, if it was already added
295                        let current_idx = if let Some(current_idx) = modules.get(&module) {
296                            *current_idx
297                        } else {
298                            let idx = graph
299                                .add_node(SingleModuleGraphNode::VisitedModule { idx, module });
300                            modules.insert(module, idx);
301                            idx
302                        };
303                        // Add the edge
304                        if let Some((parent_idx, data)) = parent_edge {
305                            graph.add_edge(parent_idx, current_idx, data);
306                        }
307                    }
308                    SingleModuleGraphBuilderNode::ChunkableReference {
309                        source,
310                        target,
311                        ref_data,
312                        ..
313                    } => {
314                        // Find the current node, if it was already added
315                        let target_idx = if let Some(target_idx) = modules.get(&target) {
316                            *target_idx
317                        } else {
318                            let target_idx = visited_modules.get(&target);
319                            let idx = graph.add_node(match target_idx {
320                                Some(idx) => SingleModuleGraphNode::VisitedModule {
321                                    idx: *idx,
322                                    module: target,
323                                },
324                                None => SingleModuleGraphNode::Module(target),
325                            });
326                            modules.insert(target, idx);
327                            idx
328                        };
329                        graph.add_edge(*modules.get(&source).unwrap(), target_idx, ref_data);
330                    }
331                }
332            }
333        }
334
335        graph.shrink_to_fit();
336
337        #[cfg(debug_assertions)]
338        {
339            use once_cell::sync::Lazy;
340            static CHECK_FOR_DUPLICATE_MODULES: Lazy<bool> = Lazy::new(|| {
341                match std::env::var_os("TURBOPACK_TEMP_DISABLE_DUPLICATE_MODULES_CHECK") {
342                    Some(v) => v != "1" && v != "true",
343                    None => true,
344                }
345            });
346            if *CHECK_FOR_DUPLICATE_MODULES {
347                let mut duplicates = Vec::new();
348                let mut set = FxHashSet::default();
349                for &module in modules.keys() {
350                    let ident = module.ident().to_string().await?;
351                    if !set.insert(ident.clone()) {
352                        duplicates.push(ident)
353                    }
354                }
355                if !duplicates.is_empty() {
356                    panic!("Duplicate module idents in graph: {duplicates:#?}");
357                }
358            }
359        }
360
361        let graph = SingleModuleGraph {
362            graph: TracedDiGraph::new(graph),
363            number_of_modules,
364            modules,
365            entries: entries.clone(),
366        }
367        .cell();
368
369        turbo_tasks::emit(ResolvedVc::upcast::<Box<dyn ImportTracer>>(
370            ModuleGraphImportTracer::new(graph).to_resolved().await?,
371        ));
372        Ok(graph)
373    }
374
375    /// Iterate over all nodes in the graph
376    pub fn iter_nodes(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + '_ {
377        self.graph.node_weights().filter_map(|n| match n {
378            SingleModuleGraphNode::Module(node) => Some(*node),
379            SingleModuleGraphNode::VisitedModule { .. } => None,
380        })
381    }
382
383    /// Returns true if the given module is in this graph and is an entry module
384    pub fn has_entry_module(&self, module: ResolvedVc<Box<dyn Module>>) -> bool {
385        if let Some(index) = self.modules.get(&module) {
386            self.graph
387                .edges_directed(*index, petgraph::Direction::Incoming)
388                .next()
389                .is_none()
390        } else {
391            false
392        }
393    }
394
395    /// Iterate over graph entry points
396    pub fn entry_modules(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Module>>> + '_ {
397        self.entries.iter().flat_map(|e| e.entries())
398    }
399
400    /// Enumerate all nodes in the graph
401    pub fn enumerate_nodes(
402        &self,
403    ) -> impl Iterator<Item = (NodeIndex, &'_ SingleModuleGraphNode)> + '_ {
404        self.graph.node_references()
405    }
406
407    pub fn read(self: &ReadRef<SingleModuleGraph>) -> ModuleGraphRef {
408        ModuleGraphRef {
409            graphs: vec![self.clone()],
410            skip_visited_module_children: true,
411        }
412    }
413
414    fn traverse_cycles<'l>(
415        &'l self,
416        edge_filter: impl Fn(&'l RefData) -> bool,
417        mut visit_cycle: impl FnMut(&[&'l ResolvedVc<Box<dyn Module>>]) -> Result<()>,
418    ) -> Result<()> {
419        // see https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
420        // but iteratively instead of recursively
421
422        #[derive(Clone)]
423        struct NodeState {
424            index: u32,
425            lowlink: u32,
426            on_stack: bool,
427        }
428        enum VisitStep {
429            UnvisitedNode(NodeIndex),
430            EdgeAfterVisit { parent: NodeIndex, child: NodeIndex },
431            AfterVisit(NodeIndex),
432        }
433        let mut node_states = vec![None; self.graph.node_bound()];
434        let mut stack = Vec::new();
435        let mut visit_stack = Vec::new();
436        let mut index = 0;
437        let mut scc = Vec::new();
438        for initial_index in self.graph.node_indices() {
439            // Skip over already visited nodes
440            if node_states[initial_index.index()].is_some() {
441                continue;
442            }
443            visit_stack.push(VisitStep::UnvisitedNode(initial_index));
444            while let Some(step) = visit_stack.pop() {
445                match step {
446                    VisitStep::UnvisitedNode(node) => {
447                        node_states[node.index()] = Some(NodeState {
448                            index,
449                            lowlink: index,
450                            on_stack: true,
451                        });
452                        index += 1;
453                        stack.push(node);
454                        visit_stack.push(VisitStep::AfterVisit(node));
455                        let mut neighbors = self.graph.neighbors(node).detach();
456                        while let Some((edge, succ)) = neighbors.next(&self.graph) {
457                            let edge_weight = self.graph.edge_weight(edge).unwrap();
458                            if !edge_filter(edge_weight) {
459                                continue;
460                            }
461                            let node_state = &node_states[succ.index()];
462                            if let Some(node_state) = node_state {
463                                if node_state.on_stack {
464                                    let index = node_state.index;
465                                    let parent_state = node_states[node.index()].as_mut().unwrap();
466                                    parent_state.lowlink = parent_state.lowlink.min(index);
467                                }
468                            } else {
469                                visit_stack.push(VisitStep::EdgeAfterVisit {
470                                    parent: node,
471                                    child: succ,
472                                });
473                                visit_stack.push(VisitStep::UnvisitedNode(succ));
474                            }
475                        }
476                    }
477                    VisitStep::EdgeAfterVisit { parent, child } => {
478                        let child_state = node_states[child.index()].as_ref().unwrap();
479                        let lowlink = child_state.lowlink;
480
481                        let parent_state = node_states[parent.index()].as_mut().unwrap();
482                        parent_state.lowlink = parent_state.lowlink.min(lowlink);
483                    }
484                    VisitStep::AfterVisit(node) => {
485                        let node_state = node_states[node.index()].as_ref().unwrap();
486                        if node_state.lowlink == node_state.index {
487                            loop {
488                                let poppped = stack.pop().unwrap();
489                                let popped_state = node_states[poppped.index()].as_mut().unwrap();
490                                popped_state.on_stack = false;
491                                if let SingleModuleGraphNode::Module(module) =
492                                    self.graph.node_weight(poppped).unwrap()
493                                {
494                                    scc.push(module);
495                                }
496                                if poppped == node {
497                                    break;
498                                }
499                            }
500                            if scc.len() > 1 {
501                                visit_cycle(&scc)?;
502                            }
503                            scc.clear();
504                        }
505                    }
506                }
507            }
508        }
509        Ok(())
510    }
511
512    /// For each issue computes a (possibly empty) list of traces from the file that produced the
513    /// issue to roots in this module graph.
514    /// There are potentially multiple traces because a given file may get assigned to multiple
515    /// modules depend on how it is used in the application.  Consider a simple utility that is used
516    /// by SSR pages, client side code, and the edge runtime.  This may lead to there being 3
517    /// traces.
518    /// The returned map is guaranteed to have an entry for every issue.
519    pub async fn compute_import_traces_for_issues(
520        &self,
521        issues: &AutoSet<ResolvedVc<Box<dyn Issue>>>,
522    ) -> Result<FxHashMap<ResolvedVc<Box<dyn Issue>>, Vec<ImportTrace>>> {
523        let issue_paths = issues
524            .iter()
525            .map(|issue| issue.file_path().owned())
526            .try_join()
527            .await?;
528        let mut file_path_to_traces: FxHashMap<FileSystemPath, Vec<ImportTrace>> =
529            FxHashMap::with_capacity_and_hasher(issue_paths.len(), Default::default());
530        // initialize an empty vec for each path we care about
531        for issue in &issue_paths {
532            file_path_to_traces.entry(issue.clone()).or_default();
533        }
534
535        {
536            let modules =
537                self.modules
538                    .iter()
539                    .map(|(module, &index)| async move {
540                        Ok((module.ident().path().owned().await?, index))
541                    })
542                    .try_join()
543                    .await?;
544            // Reverse the graph so we can find paths to roots
545            let reversed_graph = Reversed(&self.graph.0);
546            for (path, module_idx) in modules {
547                if let Entry::Occupied(mut entry) = file_path_to_traces.entry(path) {
548                    // compute the path from this index to a root of the graph.
549                    let Some((_, path)) = petgraph::algo::astar(
550                        &reversed_graph,
551                        module_idx,
552                        |n| reversed_graph.neighbors(n).next().is_none(),
553                        // Edge weights
554                        |e| match e.weight().chunking_type {
555                            // Prefer following normal imports/requires when we can
556                            ChunkingType::Parallel { .. } => 0,
557                            _ => 1,
558                        },
559                        // `astar` can be accelerated with a distance estimation heuristic, as long
560                        // as our estimate is never > the actual distance.
561                        // However we don't have a mechanism, so just
562                        // estimate 0 which essentially makes this behave like
563                        // dijktra's shortest path algorithm.  `petgraph` has an implementation of
564                        // dijkstra's but it doesn't report  paths, just distances.
565                        // NOTE: dijkstra's with integer weights can be accelerated with incredibly
566                        // efficient priority queue structures (basically with only 0 and 1 as
567                        // weights you can use a `VecDeque`!).  However,
568                        // this is unlikely to be a performance concern.
569                        // Furthermore, if computing paths _does_ become a performance concern, the
570                        // solution would be a hand written implementation of dijkstras so we can
571                        // hoist redundant work out of this loop.
572                        |_| 0,
573                    ) else {
574                        unreachable!("there must be a path to a root");
575                    };
576                    // Represent the path as a sequence of AssetIdents
577                    // TODO: consider hinting at various transitions (e.g. was this an
578                    // import/require/dynamic-import?)
579                    let path = path
580                        .into_iter()
581                        .map(async |n| {
582                            Ok(self
583                                .graph
584                                .node_weight(n)
585                                .unwrap()
586                                .module()
587                                .ident()
588                                .await?
589                                .clone())
590                        })
591                        .try_join()
592                        .await?;
593                    entry.get_mut().push(path);
594                }
595            }
596        }
597        let mut issue_to_traces: FxHashMap<ResolvedVc<Box<dyn Issue>>, Vec<ImportTrace>> =
598            FxHashMap::with_capacity_and_hasher(issues.len(), Default::default());
599        // Map filepaths back to issues
600        // We can do this by zipping the issue_paths with the issues since they are in the same
601        // order.
602        for (path, issue) in issue_paths.iter().zip(issues) {
603            if let Some(traces) = file_path_to_traces.get(path) {
604                issue_to_traces.insert(*issue, traces.clone());
605            }
606        }
607        Ok(issue_to_traces)
608    }
609}
610
611#[turbo_tasks::value]
612struct ModuleGraphImportTracer {
613    graph: ResolvedVc<SingleModuleGraph>,
614}
615
616#[turbo_tasks::value(shared)]
617struct PathToModulesMap {
618    map: FxHashMap<FileSystemPath, Vec<ResolvedVc<Box<dyn Module>>>>,
619}
620
621#[turbo_tasks::value_impl]
622impl ModuleGraphImportTracer {
623    #[turbo_tasks::function]
624    fn new(graph: ResolvedVc<SingleModuleGraph>) -> Vc<Self> {
625        Self::cell(Self { graph })
626    }
627
628    // Compute this mapping on demand since it might not always be needed.
629    #[turbo_tasks::function]
630    async fn path_to_modules(&self) -> Result<Vc<PathToModulesMap>> {
631        let path_and_modules = self
632            .graph
633            .await?
634            .modules
635            .iter()
636            .map(|(&module, _)| async move { Ok((module.ident().path().owned().await?, module)) })
637            .try_join()
638            .await?;
639        let mut map: FxHashMap<FileSystemPath, Vec<ResolvedVc<Box<dyn Module>>>> =
640            FxHashMap::default();
641        for (path, module) in path_and_modules {
642            map.entry(path).or_default().push(module)
643        }
644        Ok(PathToModulesMap::cell(PathToModulesMap { map }))
645    }
646}
647
648#[turbo_tasks::value_impl]
649impl ImportTracer for ModuleGraphImportTracer {
650    #[turbo_tasks::function]
651    async fn get_traces(self: Vc<Self>, path: FileSystemPath) -> Result<Vc<ImportTraces>> {
652        let path_to_modules = self.path_to_modules().await?;
653        let Some(modules) = path_to_modules.map.get(&path) else {
654            return Ok(Vc::default()); // This isn't unusual, the file just might not be in this
655            // graph.
656        };
657        debug_assert!(!modules.is_empty(), "modules should not be an empty vec");
658        let graph = &*self.await?.graph.await?;
659
660        let reversed_graph = Reversed(&graph.graph.0);
661        return Ok(ImportTraces::cell(ImportTraces(
662            modules
663                .iter()
664                .map(|m| async move {
665                    let Some(&module_idx) = graph.modules.get(m) else {
666                        // The only way this could really happen is if `path_to_modules` is computed
667                        // from a different graph than graph`.  Just error out.
668                        bail!("inconsistent read?")
669                    };
670                    // compute the path from this index to a root of the graph.
671                    let Some((_, path)) = petgraph::algo::astar(
672                        &reversed_graph,
673                        module_idx,
674                        |n| reversed_graph.neighbors(n).next().is_none(),
675                        // Edge weights
676                        |e| match e.weight().chunking_type {
677                            // Prefer following normal imports/requires when we can
678                            ChunkingType::Parallel { .. } => 0,
679                            _ => 1,
680                        },
681                        // `astar` can be accelerated with a distance estimation heuristic, as long
682                        // as our estimate is never > the actual distance.
683                        // However we don't have a mechanism, so just
684                        // estimate 0 which essentially makes this behave like
685                        // dijktra's shortest path algorithm.  `petgraph` has an implementation of
686                        // dijkstra's but it doesn't report  paths, just distances.
687                        // NOTE: dijkstra's with integer weights can be accelerated with incredibly
688                        // efficient priority queue structures (basically with only 0 and 1 as
689                        // weights you can use a `VecDeque`!).  However,
690                        // this is unlikely to be a performance concern.
691                        // Furthermore, if computing paths _does_ become a performance concern, the
692                        // solution would be a hand written implementation of dijkstras so we can
693                        // hoist redundant work out of this loop.
694                        |_| 0,
695                    ) else {
696                        unreachable!("there must be a path to a root");
697                    };
698
699                    // Represent the path as a sequence of AssetIdents
700                    // TODO: consider hinting at various transitions (e.g. was this an
701                    // import/require/dynamic-import?)
702                    let path = path
703                        .into_iter()
704                        .map(async |n| {
705                            graph
706                                .graph
707                                .node_weight(n)
708                                .unwrap() // This is safe since `astar`` only returns indices from the graph
709                                .module()
710                                .ident()
711                                .await
712                        })
713                        .try_join()
714                        .await?;
715                    Ok(path)
716                })
717                .try_join()
718                .await?,
719        )));
720    }
721}
722
723#[turbo_tasks::value(shared)]
724#[derive(Clone, Default)]
725pub struct ModuleGraph {
726    pub graphs: Vec<ResolvedVc<SingleModuleGraph>>,
727}
728
729#[turbo_tasks::value_impl]
730impl ModuleGraph {
731    #[turbo_tasks::function]
732    pub fn from_graphs(graphs: Vec<ResolvedVc<SingleModuleGraph>>) -> Vc<Self> {
733        Self { graphs }.cell()
734    }
735
736    #[turbo_tasks::function]
737    pub fn from_single_graph(graph: ResolvedVc<SingleModuleGraph>) -> Vc<Self> {
738        Self {
739            graphs: vec![graph],
740        }
741        .cell()
742    }
743
744    #[turbo_tasks::function]
745    pub fn from_entry_module(
746        module: ResolvedVc<Box<dyn Module>>,
747        include_traced: bool,
748    ) -> Vc<Self> {
749        Self::from_single_graph(SingleModuleGraph::new_with_entries(
750            Vc::cell(vec![ChunkGroupEntry::Entry(vec![module])]),
751            include_traced,
752        ))
753    }
754
755    #[turbo_tasks::function]
756    pub fn from_modules(modules: Vc<GraphEntries>, include_traced: bool) -> Vc<Self> {
757        Self::from_single_graph(SingleModuleGraph::new_with_entries(modules, include_traced))
758    }
759
760    #[turbo_tasks::function]
761    pub async fn chunk_group_info(self: Vc<Self>) -> Result<Vc<ChunkGroupInfo>> {
762        compute_chunk_group_info(&self.read_graphs().await?).await
763    }
764
765    #[turbo_tasks::function]
766    pub async fn merged_modules(self: Vc<Self>) -> Result<Vc<MergedModuleInfo>> {
767        compute_merged_modules(self).await
768    }
769
770    #[turbo_tasks::function]
771    pub async fn module_batches(
772        self: Vc<Self>,
773        config: Vc<BatchingConfig>,
774    ) -> Result<Vc<ModuleBatchesGraph>> {
775        compute_module_batches(self, &*config.await?).await
776    }
777
778    #[turbo_tasks::function]
779    pub async fn style_groups(
780        self: Vc<Self>,
781        chunking_context: Vc<Box<dyn ChunkingContext>>,
782        config: StyleGroupsConfig,
783    ) -> Result<Vc<StyleGroups>> {
784        compute_style_groups(self, chunking_context, &config).await
785    }
786
787    #[turbo_tasks::function]
788    pub async fn async_module_info(self: Vc<Self>) -> Result<Vc<AsyncModulesInfo>> {
789        // `compute_async_module_info` calls `module.is_self_async()`, so we need to again ignore
790        // all issues such that they aren't emitted multiple times.
791        async move {
792            let result_op = compute_async_module_info(self.to_resolved().await?);
793            let result_vc = result_op.resolve_strongly_consistent().await?;
794            result_op.drop_collectibles::<Box<dyn Issue>>();
795            anyhow::Ok(*result_vc)
796        }
797        .instrument(tracing::info_span!("compute async module info"))
798        .await
799    }
800
801    #[turbo_tasks::function]
802    pub async fn referenced_async_modules(
803        self: Vc<Self>,
804        module: ResolvedVc<Box<dyn Module>>,
805    ) -> Result<Vc<AsyncModuleInfo>> {
806        let graph_ref = self.read_graphs().await?;
807        let graphs = &graph_ref.graphs;
808        let async_modules_info = self.async_module_info().await?;
809
810        let entry = graph_ref.get_entry(module)?;
811        let referenced_modules = iter_graphs_neighbors_rev(graphs, entry)
812            .filter(|(edge_idx, _)| {
813                let ty = graphs[entry.graph_idx()]
814                    .graph
815                    .edge_weight(*edge_idx)
816                    .unwrap();
817                ty.chunking_type.is_inherit_async()
818            })
819            .map(|(_, child_idx)| anyhow::Ok(graph_ref.get_node(child_idx)?.module()))
820            .collect::<Result<Vec<_>>>()?
821            .into_iter()
822            .rev()
823            .filter(|m| async_modules_info.contains(m))
824            .map(|m| *m)
825            .collect();
826
827        Ok(AsyncModuleInfo::new(referenced_modules))
828    }
829}
830
831impl ModuleGraph {
832    pub async fn read_graphs(self: Vc<ModuleGraph>) -> Result<ModuleGraphRef> {
833        Ok(ModuleGraphRef {
834            graphs: self.await?.graphs.iter().try_join().await?,
835            skip_visited_module_children: false,
836        })
837    }
838}
839
840/// The ReadRef version of ModuleGraph. This is better for eventual consistency, as the graphs
841/// aren't awaited multiple times within the same task.
842pub struct ModuleGraphRef {
843    pub graphs: Vec<ReadRef<SingleModuleGraph>>,
844    // Whether to simply ignore SingleModuleGraphNode::VisitedModule during traversals. For single
845    // module graph usecases, this is what you want. For the whole graph, there should be an error.
846    skip_visited_module_children: bool,
847}
848
849impl ModuleGraphRef {
850    fn get_entry(&self, entry: ResolvedVc<Box<dyn Module>>) -> Result<GraphNodeIndex> {
851        let Some(idx) = self
852            .graphs
853            .iter()
854            .enumerate()
855            .find_map(|(graph_idx, graph)| {
856                graph.modules.get(&entry).map(|node_idx| GraphNodeIndex {
857                    graph_idx: u32::try_from(graph_idx).unwrap(),
858                    node_idx: *node_idx,
859                })
860            })
861        else {
862            bail!("Couldn't find entry module in module graph");
863        };
864        Ok(idx)
865    }
866
867    fn get_node(&self, entry: GraphNodeIndex) -> Result<&SingleModuleGraphNode> {
868        let graph = &self.graphs[entry.graph_idx()];
869        graph
870            .graph
871            .node_weight(entry.node_idx)
872            .context("Expected graph node")
873    }
874
875    fn should_visit_node(&self, node: &SingleModuleGraphNode) -> bool {
876        if self.skip_visited_module_children {
877            !matches!(node, SingleModuleGraphNode::VisitedModule { .. })
878        } else {
879            true
880        }
881    }
882
883    /// Returns a map of all modules in the graphs to their identifiers.
884    /// This is primarily useful for debugging.
885    pub async fn get_ids(&self) -> Result<FxHashMap<ResolvedVc<Box<dyn Module>>, ReadRef<RcStr>>> {
886        Ok(self
887            .graphs
888            .iter()
889            .flat_map(|g| g.iter_nodes())
890            .map(async |n| Ok((n, n.ident().to_string().await?)))
891            .try_join()
892            .await?
893            .into_iter()
894            .collect::<FxHashMap<_, _>>())
895    }
896
897    /// Traverses all reachable nodes exactly once and calls the visitor.
898    ///
899    /// * `entries` - The entry modules to start the traversal from
900    /// * `state` mutable state to be shared across the visitors
901    /// * `visit_preorder` - Called before visiting the children of a node.
902    ///    - Receives the module and the `state`
903    ///    - Can return [GraphTraversalAction]s to control the traversal
904    /// * `visit_postorder` - Called after visiting children of a node.
905    pub fn traverse_nodes_from_entries_dfs<S>(
906        &self,
907        entries: impl IntoIterator<Item = ResolvedVc<Box<dyn Module>>>,
908        state: &mut S,
909        visit_preorder: impl Fn(ResolvedVc<Box<dyn Module>>, &mut S) -> Result<GraphTraversalAction>,
910        mut visit_postorder: impl FnMut(ResolvedVc<Box<dyn Module>>, &mut S) -> Result<()>,
911    ) -> Result<()> {
912        let graphs = &self.graphs;
913
914        let entries = entries.into_iter().collect::<Vec<_>>();
915
916        enum Pass {
917            Visit,
918            ExpandAndVisit,
919        }
920        let mut stack: Vec<(Pass, GraphNodeIndex)> = Vec::with_capacity(entries.len());
921        for entry in entries.into_iter().rev() {
922            stack.push((Pass::ExpandAndVisit, self.get_entry(entry)?));
923        }
924        let mut expanded = FxHashSet::default();
925        while let Some((pass, current)) = stack.pop() {
926            let current_node = self.get_node(current)?;
927            match pass {
928                Pass::Visit => {
929                    visit_postorder(current_node.module(), state)?;
930                }
931                Pass::ExpandAndVisit => {
932                    if !expanded.insert(current) {
933                        continue;
934                    }
935                    let action = visit_preorder(current_node.module(), state)?;
936                    if action == GraphTraversalAction::Exclude {
937                        continue;
938                    }
939                    stack.push((Pass::Visit, current));
940                    if action == GraphTraversalAction::Continue
941                        && self.should_visit_node(current_node)
942                    {
943                        let current = current_node.target_idx().unwrap_or(current);
944                        stack.extend(
945                            iter_graphs_neighbors_rev(graphs, current)
946                                .map(|(_, child)| (Pass::ExpandAndVisit, child)),
947                        );
948                    }
949                }
950            }
951        }
952
953        Ok(())
954    }
955
956    /// Traverses all reachable edges exactly once and calls the visitor with the edge source and
957    /// target.
958    ///
959    /// This means that target nodes can be revisited (once per incoming edge).
960    ///
961    /// * `entry` - The entry module to start the traversal from
962    /// * `visitor` - Called before visiting the children of a node.
963    ///    - Receives (originating &SingleModuleGraphNode, edge &ChunkingType), target
964    ///      &SingleModuleGraphNode, state &S
965    ///    - Can return [GraphTraversalAction]s to control the traversal
966    pub fn traverse_edges_from_entries_bfs(
967        &self,
968        entries: impl IntoIterator<Item = ResolvedVc<Box<dyn Module>>>,
969        mut visitor: impl FnMut(
970            Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData)>,
971            ResolvedVc<Box<dyn Module>>,
972        ) -> Result<GraphTraversalAction>,
973    ) -> Result<()> {
974        let graphs = &self.graphs;
975
976        let mut queue = VecDeque::from(
977            entries
978                .into_iter()
979                .map(|e| self.get_entry(e))
980                .collect::<Result<Vec<_>>>()?,
981        );
982        let mut visited = FxHashSet::default();
983        for entry_node in &queue {
984            visitor(None, self.get_node(*entry_node)?.module())?;
985        }
986        while let Some(node) = queue.pop_front() {
987            if visited.insert(node) {
988                let node_weight = self.get_node(node)?;
989                let graph = &graphs[node.graph_idx()].graph;
990                for (edge, succ) in iter_graphs_neighbors_rev(graphs, node) {
991                    let succ_weight = self.get_node(succ)?;
992                    let edge_weight = graph.edge_weight(edge).unwrap();
993                    let action = visitor(
994                        Some((node_weight.module(), edge_weight)),
995                        succ_weight.module(),
996                    )?;
997                    if !self.should_visit_node(succ_weight) {
998                        continue;
999                    }
1000                    let succ = succ_weight.target_idx().unwrap_or(succ);
1001                    if !visited.contains(&succ) && action == GraphTraversalAction::Continue {
1002                        queue.push_back(succ);
1003                    }
1004                }
1005            }
1006        }
1007
1008        Ok(())
1009    }
1010
1011    /// Traverses all reachable edges exactly once and calls the visitor with the edge source and
1012    /// target.
1013    ///
1014    /// This means that target nodes can be revisited (once per incoming edge).
1015    ///
1016    /// * `entry` - The entry module to start the traversal from
1017    /// * `visitor` - Called before visiting the children of a node.
1018    ///    - Receives (originating &SingleModuleGraphNode, edge &ChunkingType), target
1019    ///      &SingleModuleGraphNode, state &S
1020    ///    - Can return [GraphTraversalAction]s to control the traversal
1021    pub fn traverse_edges_from_entry_dfs(
1022        &self,
1023        entries: impl IntoIterator<Item = ResolvedVc<Box<dyn Module>>>,
1024        mut visitor: impl FnMut(
1025            Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData)>,
1026            ResolvedVc<Box<dyn Module>>,
1027        ) -> GraphTraversalAction,
1028    ) -> Result<()> {
1029        let graphs = &self.graphs;
1030
1031        let mut stack = entries
1032            .into_iter()
1033            .map(|e| self.get_entry(e))
1034            .collect::<Result<Vec<_>>>()?;
1035        let mut visited = FxHashSet::default();
1036        for entry_node in &stack {
1037            visitor(None, self.get_node(*entry_node)?.module());
1038        }
1039        while let Some(node) = stack.pop() {
1040            if visited.insert(node) {
1041                let node_weight = self.get_node(node)?;
1042                let graph = &graphs[node.graph_idx()].graph;
1043                for (edge, succ) in iter_graphs_neighbors_rev(graphs, node) {
1044                    let succ_weight = self.get_node(succ)?;
1045                    let edge_weight = graph.edge_weight(edge).unwrap();
1046                    let action = visitor(
1047                        Some((node_weight.module(), edge_weight)),
1048                        succ_weight.module(),
1049                    );
1050                    if !self.should_visit_node(succ_weight) {
1051                        continue;
1052                    }
1053                    let succ = succ_weight.target_idx().unwrap_or(succ);
1054                    if !visited.contains(&succ) && action == GraphTraversalAction::Continue {
1055                        stack.push(succ);
1056                    }
1057                }
1058            }
1059        }
1060
1061        Ok(())
1062    }
1063
1064    /// Traverses all edges exactly once (in an unspecified order) and calls the visitor with the
1065    /// edge source and target.
1066    ///
1067    /// This means that target nodes can be revisited (once per incoming edge).
1068    ///
1069    /// * `visitor` - Called before visiting the children of a node.
1070    ///    - Receives (originating &SingleModuleGraphNode, edge &ChunkingType), target
1071    ///      &SingleModuleGraphNode
1072    pub fn traverse_all_edges_unordered(
1073        &self,
1074        mut visitor: impl FnMut(
1075            (ResolvedVc<Box<dyn Module>>, &'_ RefData),
1076            ResolvedVc<Box<dyn Module>>,
1077        ) -> Result<()>,
1078    ) -> Result<()> {
1079        for graph in &self.graphs {
1080            let graph = &graph.graph;
1081            for edge in graph.edge_references() {
1082                let source = graph.node_weight(edge.source()).unwrap().module();
1083                let target = graph.node_weight(edge.target()).unwrap().module();
1084                visitor((source, edge.weight()), target)?;
1085            }
1086        }
1087        Ok(())
1088    }
1089
1090    /// Traverses all reachable edges in dfs order. The preorder visitor can be used to
1091    /// forward state down the graph, and to skip subgraphs
1092    ///
1093    /// Use this to collect modules in evaluation order.
1094    ///
1095    /// Target nodes can be revisited (once per incoming edge) in the preorder_visitor, in the post
1096    /// order visitor they are visited exactly once with the first edge they were discovered with.
1097    /// Edges are traversed in normal order, so should correspond to reference order.
1098    ///
1099    /// * `entries` - The entry modules to start the traversal from
1100    /// * `state` - The state to be passed to the visitors
1101    /// * `visit_preorder` - Called before visiting the children of a node.
1102    ///    - Receives: (originating &SingleModuleGraphNode, edge &ChunkingType), target
1103    ///      &SingleModuleGraphNode, state &S
1104    ///    - Can return [GraphTraversalAction]s to control the traversal
1105    /// * `visit_postorder` - Called after visiting the children of a node. Return
1106    ///    - Receives: (originating &SingleModuleGraphNode, edge &ChunkingType), target
1107    ///      &SingleModuleGraphNode, state &S
1108    pub fn traverse_edges_from_entries_dfs<S>(
1109        &self,
1110        entries: impl IntoIterator<Item = ResolvedVc<Box<dyn Module>>>,
1111        state: &mut S,
1112        mut visit_preorder: impl FnMut(
1113            Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData)>,
1114            ResolvedVc<Box<dyn Module>>,
1115            &mut S,
1116        ) -> Result<GraphTraversalAction>,
1117        mut visit_postorder: impl FnMut(
1118            Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData)>,
1119            ResolvedVc<Box<dyn Module>>,
1120            &mut S,
1121        ) -> Result<()>,
1122    ) -> Result<()> {
1123        let graphs = &self.graphs;
1124
1125        let entries = entries.into_iter().collect::<Vec<_>>();
1126
1127        enum Pass {
1128            Visit,
1129            ExpandAndVisit,
1130        }
1131        #[allow(clippy::type_complexity)] // This is a temporary internal structure
1132        let mut stack: Vec<(Pass, Option<(GraphNodeIndex, EdgeIndex)>, GraphNodeIndex)> =
1133            Vec::with_capacity(entries.len());
1134        for entry in entries.into_iter().rev() {
1135            stack.push((Pass::ExpandAndVisit, None, self.get_entry(entry)?));
1136        }
1137        let mut expanded = FxHashSet::default();
1138        while let Some((pass, parent, current)) = stack.pop() {
1139            let parent_arg = match parent {
1140                Some((parent_node, parent_edge)) => Some((
1141                    self.get_node(parent_node)?.module(),
1142                    graphs[parent_node.graph_idx()]
1143                        .graph
1144                        .edge_weight(parent_edge)
1145                        .unwrap(),
1146                )),
1147                None => None,
1148            };
1149            let current_node = self.get_node(current)?;
1150            match pass {
1151                Pass::Visit => {
1152                    visit_postorder(parent_arg, current_node.module(), state)?;
1153                }
1154                Pass::ExpandAndVisit => {
1155                    let action = visit_preorder(parent_arg, current_node.module(), state)?;
1156                    if action == GraphTraversalAction::Exclude {
1157                        continue;
1158                    }
1159                    stack.push((Pass::Visit, parent, current));
1160                    if action == GraphTraversalAction::Continue
1161                        && expanded.insert(current)
1162                        && self.should_visit_node(current_node)
1163                    {
1164                        let current = current_node.target_idx().unwrap_or(current);
1165                        stack.extend(iter_graphs_neighbors_rev(graphs, current).map(
1166                            |(edge, child)| (Pass::ExpandAndVisit, Some((current, edge)), child),
1167                        ));
1168                    }
1169                }
1170            }
1171        }
1172
1173        Ok(())
1174    }
1175
1176    /// Traverse all cycles in the graph (where the edge filter returns true for the whole cycle)
1177    /// and call the visitor with the nodes in the cycle.
1178    pub fn traverse_cycles(
1179        &self,
1180        edge_filter: impl Fn(&RefData) -> bool,
1181        mut visit_cycle: impl FnMut(&[&ResolvedVc<Box<dyn Module>>]) -> Result<()>,
1182    ) -> Result<()> {
1183        for graph in &self.graphs {
1184            graph.traverse_cycles(&edge_filter, &mut visit_cycle)?;
1185        }
1186        Ok(())
1187    }
1188
1189    /// Traverses all reachable nodes and also continue revisiting them as long the visitor returns
1190    /// GraphTraversalAction::Continue. The visitor is responsible for the runtime complexity and
1191    /// eventual termination of the traversal. This corresponds to computing a fixed point state for
1192    /// the graph.
1193    ///
1194    /// Nodes are (re)visited according to the returned priority of the node, prioritizing high
1195    /// values. This priority is intended to be used a heuristic to reduce the number of
1196    /// retraversals.
1197    ///
1198    /// * `entries` - The entry modules to start the traversal from
1199    /// * `state` - The state to be passed to the callbacks
1200    /// * `visit` - Called for a specific edge
1201    ///    - Receives: (originating &SingleModuleGraphNode, edge &ChunkingType), target
1202    ///      &SingleModuleGraphNode, state &S
1203    ///    - Return [GraphTraversalAction]s to control the traversal
1204    /// * `priority` - Called for before visiting the children of a node to determine its priority.
1205    ///    - Receives: target &SingleModuleGraphNode, state &S
1206    ///    - Return a priority value for the node
1207    ///
1208    /// Returns the number of node visits (i.e. higher than the node count if there are
1209    /// retraversals).
1210    pub fn traverse_edges_fixed_point_with_priority<S, P: Ord>(
1211        &self,
1212        entries: impl IntoIterator<Item = (ResolvedVc<Box<dyn Module>>, P)>,
1213        state: &mut S,
1214        mut visit: impl FnMut(
1215            Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData)>,
1216            ResolvedVc<Box<dyn Module>>,
1217            &mut S,
1218        ) -> Result<GraphTraversalAction>,
1219        priority: impl Fn(ResolvedVc<Box<dyn Module>>, &mut S) -> Result<P>,
1220    ) -> Result<usize> {
1221        let graphs = &self.graphs;
1222        if self.skip_visited_module_children {
1223            panic!(
1224                "traverse_edges_fixed_point_with_priority musn't be called on individual graphs"
1225            );
1226        }
1227
1228        #[derive(PartialEq, Eq)]
1229        struct NodeWithPriority<T: Ord> {
1230            node: GraphNodeIndex,
1231            priority: T,
1232        }
1233        impl<T: Ord> PartialOrd for NodeWithPriority<T> {
1234            fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1235                Some(self.cmp(other))
1236            }
1237        }
1238        impl<T: Ord> Ord for NodeWithPriority<T> {
1239            fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1240                // BinaryHeap prioritizes high values
1241
1242                self.priority
1243                    .cmp(&other.priority)
1244                    // include GraphNodeIndex for total and deterministic ordering
1245                    .then(other.node.cmp(&self.node))
1246            }
1247        }
1248
1249        let mut queue_set = FxHashSet::default();
1250        let mut queue = BinaryHeap::from_iter(
1251            entries
1252                .into_iter()
1253                .map(|(m, priority)| {
1254                    Ok(NodeWithPriority {
1255                        node: self.get_entry(m)?,
1256                        priority,
1257                    })
1258                })
1259                .collect::<Result<Vec<_>>>()?,
1260        );
1261
1262        for entry_node in &queue {
1263            visit(None, self.get_node(entry_node.node)?.module(), state)?;
1264        }
1265
1266        let mut visit_count = 0usize;
1267        while let Some(NodeWithPriority { node, .. }) = queue.pop() {
1268            queue_set.remove(&node);
1269            let node_weight = self.get_node(node)?;
1270            let node = node_weight.target_idx().unwrap_or(node);
1271
1272            visit_count += 1;
1273
1274            let graph = &graphs[node.graph_idx()].graph;
1275            for (edge, succ) in iter_graphs_neighbors_rev(graphs, node) {
1276                let succ_weight = self.get_node(succ)?;
1277
1278                let edge_weight = graph.edge_weight(edge).unwrap();
1279                let action = visit(
1280                    Some((node_weight.module(), edge_weight)),
1281                    succ_weight.module(),
1282                    state,
1283                )?;
1284
1285                let succ = succ_weight.target_idx().unwrap_or(succ);
1286                if action == GraphTraversalAction::Continue && queue_set.insert(succ) {
1287                    queue.push(NodeWithPriority {
1288                        node: succ,
1289                        priority: priority(succ_weight.module(), state)?,
1290                    });
1291                }
1292            }
1293        }
1294
1295        Ok(visit_count)
1296    }
1297}
1298
1299/// Iterate the edges of a node REVERSED!
1300fn iter_graphs_neighbors_rev(
1301    graphs: &[ReadRef<SingleModuleGraph>],
1302    node: GraphNodeIndex,
1303) -> impl Iterator<Item = (EdgeIndex, GraphNodeIndex)> + '_ {
1304    let graph = &*graphs[node.graph_idx()].graph;
1305
1306    if cfg!(debug_assertions) {
1307        let node_weight = graph.node_weight(node.node_idx).unwrap();
1308        if let SingleModuleGraphNode::VisitedModule { .. } = node_weight {
1309            panic!("iter_graphs_neighbors_rev called on VisitedModule node");
1310        }
1311    }
1312
1313    let mut walker = graph.neighbors(node.node_idx).detach();
1314    std::iter::from_fn(move || {
1315        walker.next(graph).map(|(edge_idx, succ_idx)| {
1316            (
1317                edge_idx,
1318                GraphNodeIndex {
1319                    graph_idx: node.graph_idx,
1320                    node_idx: succ_idx,
1321                },
1322            )
1323        })
1324    })
1325}
1326
1327#[turbo_tasks::value_impl]
1328impl SingleModuleGraph {
1329    #[turbo_tasks::function]
1330    pub async fn new_with_entries(
1331        entries: Vc<GraphEntries>,
1332        include_traced: bool,
1333    ) -> Result<Vc<Self>> {
1334        SingleModuleGraph::new_inner(&*entries.await?, &Default::default(), include_traced).await
1335    }
1336
1337    #[turbo_tasks::function]
1338    pub async fn new_with_entries_visited(
1339        entries: Vc<GraphEntries>,
1340        visited_modules: Vc<VisitedModules>,
1341        include_traced: bool,
1342    ) -> Result<Vc<Self>> {
1343        SingleModuleGraph::new_inner(
1344            &*entries.await?,
1345            &visited_modules.await?.modules,
1346            include_traced,
1347        )
1348        .await
1349    }
1350
1351    #[turbo_tasks::function]
1352    pub async fn new_with_entries_visited_intern(
1353        // This must not be a Vc<Vec<_>> to ensure layout segment optimization hits the cache
1354        entries: GraphEntriesT,
1355        visited_modules: Vc<VisitedModules>,
1356        include_traced: bool,
1357    ) -> Result<Vc<Self>> {
1358        SingleModuleGraph::new_inner(&entries, &visited_modules.await?.modules, include_traced)
1359            .await
1360    }
1361}
1362
1363#[derive(Clone, Debug, Serialize, Deserialize, TraceRawVcs, NonLocalValue)]
1364pub enum SingleModuleGraphNode {
1365    Module(ResolvedVc<Box<dyn Module>>),
1366    // Models a module that is referenced but has already been visited by an earlier graph.
1367    VisitedModule {
1368        idx: GraphNodeIndex,
1369        module: ResolvedVc<Box<dyn Module>>,
1370    },
1371}
1372
1373impl SingleModuleGraphNode {
1374    pub fn module(&self) -> ResolvedVc<Box<dyn Module>> {
1375        match self {
1376            SingleModuleGraphNode::Module(module) => *module,
1377            SingleModuleGraphNode::VisitedModule { module, .. } => *module,
1378        }
1379    }
1380    pub fn target_idx(&self) -> Option<GraphNodeIndex> {
1381        match self {
1382            SingleModuleGraphNode::VisitedModule { idx, .. } => Some(*idx),
1383            SingleModuleGraphNode::Module(_) => None,
1384        }
1385    }
1386}
1387
1388#[derive(PartialEq, Eq, Debug)]
1389pub enum GraphTraversalAction {
1390    /// Continue visiting children
1391    Continue,
1392    /// Skip the immediate children, but visit the node in postorder
1393    Skip,
1394    /// Skip the immediate children and the node in postorder
1395    Exclude,
1396}
1397
1398// These nodes are created while walking the Turbopack modules references, and are used to then
1399// afterwards build the SingleModuleGraph.
1400#[derive(Clone, Hash, PartialEq, Eq)]
1401enum SingleModuleGraphBuilderNode {
1402    /// This edge is represented as a node: source Module -> ChunkableReference ->  target Module
1403    ChunkableReference {
1404        ref_data: RefData,
1405        source: ResolvedVc<Box<dyn Module>>,
1406        target: ResolvedVc<Box<dyn Module>>,
1407        // These two fields are only used for tracing. Derived from `source.ident()` and
1408        // `target.ident()`
1409        source_ident: Option<ReadRef<RcStr>>,
1410        target_ident: Option<ReadRef<RcStr>>,
1411    },
1412    /// A regular module
1413    Module {
1414        module: ResolvedVc<Box<dyn Module>>,
1415        // module.ident().to_string(), eagerly computed for tracing
1416        ident: Option<ReadRef<RcStr>>,
1417    },
1418    /// A reference to a module that is already listed in visited_modules
1419    VisitedModule {
1420        module: ResolvedVc<Box<dyn Module>>,
1421        idx: GraphNodeIndex,
1422    },
1423}
1424
1425impl SingleModuleGraphBuilderNode {
1426    async fn new_module(emit_spans: bool, module: ResolvedVc<Box<dyn Module>>) -> Result<Self> {
1427        Ok(Self::Module {
1428            module,
1429            ident: if emit_spans {
1430                // INVALIDATION: we don't need to invalidate when the span name changes
1431                Some(module.ident_string().untracked().await?)
1432            } else {
1433                None
1434            },
1435        })
1436    }
1437    async fn new_chunkable_ref(
1438        emit_spans: bool,
1439        source: ResolvedVc<Box<dyn Module>>,
1440        target: ResolvedVc<Box<dyn Module>>,
1441        ref_data: RefData,
1442    ) -> Result<Self> {
1443        Ok(Self::ChunkableReference {
1444            ref_data,
1445            source,
1446            source_ident: if emit_spans {
1447                // INVALIDATION: we don't need to invalidate when the span name changes
1448                Some(source.ident_string().untracked().await?)
1449            } else {
1450                None
1451            },
1452            target,
1453            target_ident: if emit_spans {
1454                // INVALIDATION: we don't need to invalidate when the span name changes
1455                Some(target.ident_string().untracked().await?)
1456            } else {
1457                None
1458            },
1459        })
1460    }
1461    fn new_visited_module(module: ResolvedVc<Box<dyn Module>>, idx: GraphNodeIndex) -> Self {
1462        Self::VisitedModule { module, idx }
1463    }
1464}
1465struct SingleModuleGraphBuilderEdge {
1466    to: SingleModuleGraphBuilderNode,
1467    export: ExportUsage,
1468}
1469
1470/// The chunking type that occurs most often, is handled more efficiently by not creating
1471/// intermediate SingleModuleGraphBuilderNode::ChunkableReference nodes.
1472const COMMON_CHUNKING_TYPE: ChunkingType = ChunkingType::Parallel {
1473    inherit_async: true,
1474    hoisted: true,
1475};
1476
1477struct SingleModuleGraphBuilder<'a> {
1478    visited_modules: &'a FxIndexMap<ResolvedVc<Box<dyn Module>>, GraphNodeIndex>,
1479
1480    emit_spans: bool,
1481
1482    /// Whether to walk ChunkingType::Traced references
1483    include_traced: bool,
1484}
1485impl Visit<(SingleModuleGraphBuilderNode, ExportUsage)> for SingleModuleGraphBuilder<'_> {
1486    type Edge = SingleModuleGraphBuilderEdge;
1487    type EdgesIntoIter = Vec<Self::Edge>;
1488    type EdgesFuture = impl Future<Output = Result<Self::EdgesIntoIter>>;
1489
1490    fn visit(
1491        &mut self,
1492        edge: Self::Edge,
1493    ) -> VisitControlFlow<(SingleModuleGraphBuilderNode, ExportUsage)> {
1494        match edge.to {
1495            SingleModuleGraphBuilderNode::Module { .. } => {
1496                VisitControlFlow::Continue((edge.to, edge.export))
1497            }
1498            SingleModuleGraphBuilderNode::ChunkableReference { ref ref_data, .. } => {
1499                match &ref_data.chunking_type {
1500                    ChunkingType::Traced => VisitControlFlow::Skip((edge.to, edge.export)),
1501                    _ => VisitControlFlow::Continue((edge.to, edge.export)),
1502                }
1503            }
1504            // Module was already visited previously
1505            SingleModuleGraphBuilderNode::VisitedModule { .. } => {
1506                VisitControlFlow::Skip((edge.to, edge.export))
1507            }
1508        }
1509    }
1510
1511    fn edges(
1512        &mut self,
1513        // The `skip_duplicates_with_key()` above ensures only a single `edges()` call per module
1514        // (and not per `(module, export)` pair), so the export must not be read here!
1515        (node, _): &(SingleModuleGraphBuilderNode, ExportUsage),
1516    ) -> Self::EdgesFuture {
1517        // Destructure beforehand to not have to clone the whole node when entering the async block
1518        let (module, chunkable_ref_target) = match node {
1519            SingleModuleGraphBuilderNode::Module { module, .. } => (Some(*module), None),
1520            SingleModuleGraphBuilderNode::ChunkableReference {
1521                target, ref_data, ..
1522            } => (None, Some((*target, ref_data.export.clone()))),
1523            // These are always skipped in `visit()`
1524            SingleModuleGraphBuilderNode::VisitedModule { .. } => unreachable!(),
1525        };
1526        let visited_modules = self.visited_modules;
1527        let emit_spans = self.emit_spans;
1528        let include_traced = self.include_traced;
1529        async move {
1530            Ok(match (module, chunkable_ref_target) {
1531                (Some(module), None) => {
1532                    let refs_cell = primary_chunkable_referenced_modules(*module, include_traced);
1533                    let refs = match refs_cell.await {
1534                        Ok(refs) => refs,
1535                        Err(e) => {
1536                            return Err(e.context(module.ident().to_string().await?));
1537                        }
1538                    };
1539
1540                    refs.iter()
1541                        .flat_map(|(ty, export, modules)| {
1542                            modules.iter().map(|m| (ty.clone(), export.clone(), *m))
1543                        })
1544                        .map(async |(ty, export, target)| {
1545                            let to = if ty == COMMON_CHUNKING_TYPE {
1546                                if let Some(idx) = visited_modules.get(&target) {
1547                                    SingleModuleGraphBuilderNode::new_visited_module(target, *idx)
1548                                } else {
1549                                    SingleModuleGraphBuilderNode::new_module(emit_spans, target)
1550                                        .await?
1551                                }
1552                            } else {
1553                                SingleModuleGraphBuilderNode::new_chunkable_ref(
1554                                    emit_spans,
1555                                    module,
1556                                    target,
1557                                    RefData {
1558                                        chunking_type: ty,
1559                                        export: export.clone(),
1560                                    },
1561                                )
1562                                .await?
1563                            };
1564                            Ok(SingleModuleGraphBuilderEdge { to, export })
1565                        })
1566                        .try_join()
1567                        .await?
1568                }
1569                (None, Some((chunkable_ref_target, export))) => {
1570                    vec![SingleModuleGraphBuilderEdge {
1571                        to: if let Some(idx) = visited_modules.get(&chunkable_ref_target) {
1572                            SingleModuleGraphBuilderNode::new_visited_module(
1573                                chunkable_ref_target,
1574                                *idx,
1575                            )
1576                        } else {
1577                            SingleModuleGraphBuilderNode::new_module(
1578                                emit_spans,
1579                                chunkable_ref_target,
1580                            )
1581                            .await?
1582                        },
1583                        export,
1584                    }]
1585                }
1586                _ => unreachable!(),
1587            })
1588        }
1589    }
1590
1591    fn span(&mut self, (node, _): &(SingleModuleGraphBuilderNode, ExportUsage)) -> tracing::Span {
1592        if !self.emit_spans {
1593            return Span::current();
1594        }
1595
1596        match node {
1597            SingleModuleGraphBuilderNode::Module {
1598                ident: Some(ident), ..
1599            } => {
1600                tracing::info_span!("module", name = display(ident))
1601            }
1602            SingleModuleGraphBuilderNode::ChunkableReference {
1603                ref_data,
1604                source_ident: Some(source_ident),
1605                target_ident: Some(target_ident),
1606                ..
1607            } => match &ref_data.chunking_type {
1608                ChunkingType::Parallel {
1609                    inherit_async: false,
1610                    ..
1611                } => Span::current(),
1612                _ => {
1613                    tracing::info_span!(
1614                        "chunkable reference",
1615                        ty = debug(&ref_data.chunking_type),
1616                        source = display(source_ident),
1617                        target = display(target_ident)
1618                    )
1619                }
1620            },
1621            SingleModuleGraphBuilderNode::VisitedModule { .. } => {
1622                tracing::info_span!("visited module")
1623            }
1624            _ => Span::current(),
1625        }
1626    }
1627}
1628
1629#[cfg(test)]
1630pub mod tests {
1631    use anyhow::Result;
1632    use rustc_hash::FxHashMap;
1633    use turbo_rcstr::{RcStr, rcstr};
1634    use turbo_tasks::{ResolvedVc, TryJoinIterExt, Vc};
1635    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
1636    use turbo_tasks_fs::{FileSystem, FileSystemPath, VirtualFileSystem};
1637
1638    use crate::{
1639        asset::{Asset, AssetContent},
1640        ident::AssetIdent,
1641        module::Module,
1642        module_graph::{
1643            GraphEntries, GraphTraversalAction, ModuleGraph, ModuleGraphRef, SingleModuleGraph,
1644            chunk_group_info::ChunkGroupEntry,
1645        },
1646        reference::{ModuleReference, ModuleReferences, SingleChunkableModuleReference},
1647        resolve::ExportUsage,
1648    };
1649
1650    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1651    async fn test_traverse_dfs_from_entries_diamond() {
1652        run_graph_test(
1653            vec![rcstr!("a.js")],
1654            {
1655                let mut deps = FxHashMap::default();
1656                // A classic diamond dependency on d
1657                deps.insert(rcstr!("a.js"), vec![rcstr!("b.js"), rcstr!("c.js")]);
1658                deps.insert(rcstr!("b.js"), vec![rcstr!("d.js")]);
1659                deps.insert(rcstr!("c.js"), vec![rcstr!("d.js")]);
1660                deps
1661            },
1662            |graph, entry_modules, module_to_name| {
1663                let mut preorder_visits = Vec::new();
1664                let mut postorder_visits = Vec::new();
1665
1666                graph.traverse_edges_from_entries_dfs(
1667                    entry_modules,
1668                    &mut (),
1669                    |parent, target, _| {
1670                        preorder_visits.push((
1671                            parent.map(|(node, _)| module_to_name.get(&node).unwrap().clone()),
1672                            module_to_name.get(&target).unwrap().clone(),
1673                        ));
1674                        Ok(GraphTraversalAction::Continue)
1675                    },
1676                    |parent, target, _| {
1677                        postorder_visits.push((
1678                            parent.map(|(node, _)| module_to_name.get(&node).unwrap().clone()),
1679                            module_to_name.get(&target).unwrap().clone(),
1680                        ));
1681                        Ok(())
1682                    },
1683                )?;
1684                assert_eq!(
1685                    vec![
1686                        (None, rcstr!("a.js")),
1687                        (Some(rcstr!("a.js")), rcstr!("b.js")),
1688                        (Some(rcstr!("b.js")), rcstr!("d.js")),
1689                        (Some(rcstr!("a.js")), rcstr!("c.js")),
1690                        (Some(rcstr!("c.js")), rcstr!("d.js"))
1691                    ],
1692                    preorder_visits
1693                );
1694                assert_eq!(
1695                    vec![
1696                        (Some(rcstr!("b.js")), rcstr!("d.js")),
1697                        (Some(rcstr!("a.js")), rcstr!("b.js")),
1698                        (Some(rcstr!("c.js")), rcstr!("d.js")),
1699                        (Some(rcstr!("a.js")), rcstr!("c.js")),
1700                        (None, rcstr!("a.js"))
1701                    ],
1702                    postorder_visits
1703                );
1704                Ok(())
1705            },
1706        )
1707        .await;
1708    }
1709
1710    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1711    async fn test_traverse_dfs_from_entries_cycle() {
1712        run_graph_test(
1713            vec![rcstr!("a.js")],
1714            {
1715                let mut deps = FxHashMap::default();
1716                // A cycle of length 3
1717                deps.insert(rcstr!("a.js"), vec![rcstr!("b.js")]);
1718                deps.insert(rcstr!("b.js"), vec![rcstr!("c.js")]);
1719                deps.insert(rcstr!("c.js"), vec![rcstr!("a.js")]);
1720                deps
1721            },
1722            |graph, entry_modules, module_to_name| {
1723                let mut preorder_visits = Vec::new();
1724                let mut postorder_visits = Vec::new();
1725
1726                graph.traverse_edges_from_entries_dfs(
1727                    entry_modules,
1728                    &mut (),
1729                    |parent, target, _| {
1730                        preorder_visits.push((
1731                            parent.map(|(node, _)| module_to_name.get(&node).unwrap().clone()),
1732                            module_to_name.get(&target).unwrap().clone(),
1733                        ));
1734                        Ok(GraphTraversalAction::Continue)
1735                    },
1736                    |parent, target, _| {
1737                        postorder_visits.push((
1738                            parent.map(|(node, _)| module_to_name.get(&node).unwrap().clone()),
1739                            module_to_name.get(&target).unwrap().clone(),
1740                        ));
1741                        Ok(())
1742                    },
1743                )?;
1744                assert_eq!(
1745                    vec![
1746                        (None, rcstr!("a.js")),
1747                        (Some(rcstr!("a.js")), rcstr!("b.js")),
1748                        (Some(rcstr!("b.js")), rcstr!("c.js")),
1749                        (Some(rcstr!("c.js")), rcstr!("a.js")),
1750                    ],
1751                    preorder_visits
1752                );
1753                assert_eq!(
1754                    vec![
1755                        (Some(rcstr!("c.js")), rcstr!("a.js")),
1756                        (Some(rcstr!("b.js")), rcstr!("c.js")),
1757                        (Some(rcstr!("a.js")), rcstr!("b.js")),
1758                        (None, rcstr!("a.js"))
1759                    ],
1760                    postorder_visits
1761                );
1762                Ok(())
1763            },
1764        )
1765        .await;
1766    }
1767
1768    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1769    async fn test_traverse_edges_fixed_point_with_priority_cycle() {
1770        run_graph_test(
1771            vec![rcstr!("a.js")],
1772            {
1773                let mut deps = FxHashMap::default();
1774                // A cycle of length 3
1775                deps.insert(rcstr!("a.js"), vec![rcstr!("b.js")]);
1776                deps.insert(rcstr!("b.js"), vec![rcstr!("c.js")]);
1777                deps.insert(rcstr!("c.js"), vec![rcstr!("a.js")]);
1778                deps
1779            },
1780            |graph, entry_modules, module_to_name| {
1781                let mut visits = Vec::new();
1782                let mut count = 0;
1783
1784                graph.traverse_edges_fixed_point_with_priority(
1785                    entry_modules.into_iter().map(|m| (m, 0)),
1786                    &mut (),
1787                    |parent, target, _| {
1788                        visits.push((
1789                            parent.map(|(node, _)| module_to_name.get(&node).unwrap().clone()),
1790                            module_to_name.get(&target).unwrap().clone(),
1791                        ));
1792                        count += 1;
1793
1794                        // We are a cycle so we need to break the loop eventually
1795                        Ok(if count < 6 {
1796                            GraphTraversalAction::Continue
1797                        } else {
1798                            GraphTraversalAction::Skip
1799                        })
1800                    },
1801                    |_, _| Ok(0),
1802                )?;
1803                assert_eq!(
1804                    vec![
1805                        (None, rcstr!("a.js")),
1806                        (Some(rcstr!("a.js")), rcstr!("b.js")),
1807                        (Some(rcstr!("b.js")), rcstr!("c.js")),
1808                        (Some(rcstr!("c.js")), rcstr!("a.js")),
1809                        // we start following the cycle again
1810                        (Some(rcstr!("a.js")), rcstr!("b.js")),
1811                        (Some(rcstr!("b.js")), rcstr!("c.js")),
1812                    ],
1813                    visits
1814                );
1815
1816                Ok(())
1817            },
1818        )
1819        .await;
1820    }
1821    #[turbo_tasks::value(shared)]
1822    struct TestRepo {
1823        repo: FxHashMap<FileSystemPath, Vec<FileSystemPath>>,
1824    }
1825    #[turbo_tasks::value]
1826    struct MockModule {
1827        path: FileSystemPath,
1828        repo: ResolvedVc<TestRepo>,
1829    }
1830    #[turbo_tasks::value_impl]
1831    impl MockModule {
1832        #[turbo_tasks::function]
1833        fn new(path: FileSystemPath, repo: ResolvedVc<TestRepo>) -> Vc<Self> {
1834            Self { path, repo }.cell()
1835        }
1836    }
1837
1838    #[turbo_tasks::value_impl]
1839    impl Asset for MockModule {
1840        #[turbo_tasks::function]
1841        fn content(&self) -> Vc<AssetContent> {
1842            panic!("MockModule::content shouldn't be called")
1843        }
1844    }
1845
1846    #[turbo_tasks::value_impl]
1847    impl Module for MockModule {
1848        #[turbo_tasks::function]
1849        fn ident(&self) -> Vc<AssetIdent> {
1850            AssetIdent::from_path(self.path.clone())
1851        }
1852
1853        #[turbo_tasks::function]
1854        async fn references(&self) -> Result<Vc<ModuleReferences>> {
1855            let repo = self.repo.await?;
1856            let references = match repo.repo.get(&self.path) {
1857                Some(deps) => {
1858                    deps.iter()
1859                        .map(|p| {
1860                            Vc::upcast::<Box<dyn ModuleReference>>(
1861                                SingleChunkableModuleReference::new(
1862                                    Vc::upcast(MockModule::new(p.clone(), *self.repo)),
1863                                    rcstr!("normal-dep"),
1864                                    ExportUsage::all(),
1865                                ),
1866                            )
1867                            .to_resolved()
1868                        })
1869                        .try_join()
1870                        .await?
1871                }
1872                None => vec![],
1873            };
1874
1875            Ok(Vc::cell(references))
1876        }
1877    }
1878
1879    /// Constructs a graph based on the provided dependency adjacency lists and calls the given test
1880    /// function.
1881    ///
1882    /// # Parameters
1883    /// - `entries`: A vector of entry module names (as `RcStr`). These are the starting points for
1884    ///   the graph.
1885    /// - `graph`: A map from module name (`RcStr`) to a vector of its dependency module names
1886    ///   (`RcStr`). Represents the adjacency list of the graph.
1887    /// - `test_fn`: A function that is called with:
1888    ///     - `ReadRef<SingleModuleGraph>`: The constructed module graph.
1889    ///     - `Vec<ResolvedVc<Box<dyn Module>>>`: The resolved entry modules.
1890    ///     - `FxHashMap<ResolvedVc<Box<dyn Module>>, RcStr>`: A mapping from module to its name for
1891    ///       easier analysis in tests.
1892    async fn run_graph_test(
1893        entries: Vec<RcStr>,
1894        graph: FxHashMap<RcStr, Vec<RcStr>>,
1895        test_fn: impl FnOnce(
1896            ModuleGraphRef,
1897            Vec<ResolvedVc<Box<dyn Module>>>,
1898            FxHashMap<ResolvedVc<Box<dyn Module>>, RcStr>,
1899        ) -> Result<()>
1900        + Send
1901        + 'static,
1902    ) {
1903        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
1904            BackendOptions::default(),
1905            noop_backing_storage(),
1906        ));
1907        tt.run_once(async move {
1908            let fs = VirtualFileSystem::new_with_name(rcstr!("test"));
1909            let root = fs.root().await?;
1910
1911            let repo = TestRepo {
1912                repo: graph
1913                    .iter()
1914                    .map(|(k, v)| {
1915                        (
1916                            root.join(k).unwrap(),
1917                            v.iter().map(|f| root.join(f).unwrap()).collect(),
1918                        )
1919                    })
1920                    .collect(),
1921            }
1922            .cell();
1923            let entry_modules = entries
1924                .iter()
1925                .map(|e| {
1926                    Vc::upcast::<Box<dyn Module>>(MockModule::new(root.join(e).unwrap(), repo))
1927                        .to_resolved()
1928                })
1929                .try_join()
1930                .await?;
1931            let graph = SingleModuleGraph::new_with_entries(
1932                GraphEntries::cell(GraphEntries(vec![ChunkGroupEntry::Entry(
1933                    entry_modules.clone(),
1934                )])),
1935                false,
1936            );
1937
1938            // Create a simple name mapping to make analyzing the visitors easier.
1939            // Technically they could always pull this name off of the
1940            // `module.ident().await?.path.path` themselves but you cannot `await` in visitors.
1941            let module_to_name = graph
1942                .await?
1943                .modules
1944                .keys()
1945                .map(|m| async move { Ok((*m, m.ident().await?.path.path.clone())) })
1946                .try_join()
1947                .await?
1948                .into_iter()
1949                .collect();
1950            test_fn(
1951                ModuleGraph::from_single_graph(graph).read_graphs().await?,
1952                entry_modules,
1953                module_to_name,
1954            )
1955        })
1956        .await
1957        .unwrap();
1958    }
1959}