Skip to main content

turbopack_core/module_graph/
collect.rs

1use std::{borrow::Cow, cmp::Reverse};
2
3use anyhow::{Context, Result, bail};
4use rustc_hash::FxHashMap;
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryJoinIterExt, Vc};
7
8use crate::{
9    chunk::ChunkingType,
10    emit_collect::CollectingModule,
11    issue::{IssueExt, module::ModuleIssue},
12    module::Module,
13    module_graph::{
14        GraphTraversalAction, ModuleGraph, RefData,
15        chunk_group_info::{ChunkGroupEntry, RoaringBitmapWrapper},
16    },
17};
18
19#[turbo_tasks::value(transparent, cell = "keyed")]
20#[allow(clippy::type_complexity)]
21/// Additional references that need to be added to the graph due to collecting modules. They
22/// are conditional based on the current page being chunked.
23///
24/// (Collecting Module) -> Vec<(ChunkGroup::Entry Modules, Vec<(Reference, Collected Module)>)>
25pub struct CollectedModules(
26    #[bincode(with = "turbo_bincode::indexmap")]
27    FxIndexMap<
28        ResolvedVc<Box<dyn Module>>,
29        Vec<(
30            Vec<ResolvedVc<Box<dyn Module>>>,
31            Vec<(RefData, ResolvedVc<Box<dyn Module>>)>,
32        )>,
33    >,
34);
35
36// The goal is:
37// 1. Find all ChunkingType::Emitted references
38// 2. Find all CollectingModules
39// 3. For each CollectingModule, collect all emitted references within the same ChunkGroup::Entry as
40//    the CollectingModule where the .namespace() matches.
41#[tracing::instrument(level = "info", name = "compute emit-collect", skip_all)]
42pub async fn collect_graph(graph: Vc<ModuleGraph>) -> Result<Vc<CollectedModules>> {
43    let graph = graph.await?;
44    let graphs = &graph.graphs;
45
46    let module_count = graphs.iter().map(|g| g.graph.node_count()).sum::<usize>();
47
48    let entry_groups = graph
49        .all_chunk_group_entries()
50        .flat_map(|g| match g {
51            ChunkGroupEntry::Entry {
52                modules: entries, ..
53            } => Some(entries),
54            _ => None,
55        })
56        .collect::<Vec<_>>();
57    let entry_group_modules = entry_groups
58        .iter()
59        .flat_map(|entries| entries.iter().copied())
60        .collect::<Vec<_>>();
61
62    // Create a mapping of module -> ChunkGroupEntry::Entry that import it
63    let mut module_entry_membership: FxHashMap<ResolvedVc<Box<dyn Module>>, RoaringBitmapWrapper> =
64        FxHashMap::with_capacity_and_hasher(module_count, Default::default());
65    for (i, entries) in entry_groups.iter().enumerate() {
66        for entry in *entries {
67            module_entry_membership
68                .entry(*entry)
69                .or_default()
70                .insert(i as u32);
71        }
72    }
73
74    // First, rank every module in reverse postorder (so that a module is visited after the
75    // modules importing it), and initialize the membership of every reachable module.
76    let module_rank = graph.reverse_postorder_ranks(entry_group_modules.iter().copied())?;
77    for module in module_rank.keys() {
78        module_entry_membership.entry(*module).or_default();
79    }
80
81    // - Discover all collecting module
82    // - Discover all emitted references
83    // - Set module_entry_membership
84    let mut collecting_modules: FxIndexSet<ResolvedVc<Box<dyn CollectingModule>>> =
85        FxIndexSet::default();
86    let mut emitted_references: FxIndexSet<(&RefData, ResolvedVc<Box<dyn Module>>)> =
87        FxIndexSet::default();
88    graph.traverse_edges_fixed_point_with_priority(
89        entry_group_modules
90            .iter()
91            .map(|e| {
92                Ok((
93                    *e,
94                    Reverse(*module_rank.get(e).context("Module rank not found")?),
95                ))
96            })
97            .collect::<Result<Vec<_>>>()?,
98        &mut (&mut module_entry_membership, &mut emitted_references),
99        |parent_info: Option<(ResolvedVc<Box<dyn Module>>, &'_ RefData, _)>,
100         node: ResolvedVc<Box<dyn Module>>,
101         _,
102         (module_entry_membership, emitted_references)|
103         -> Result<GraphTraversalAction> {
104            if let Some(node) = ResolvedVc::try_downcast::<Box<dyn CollectingModule>>(node) {
105                collecting_modules.insert(node);
106            }
107
108            let Some((parent, ref_data, _)) = parent_info else {
109                // An entry module
110                return Ok(GraphTraversalAction::Continue);
111            };
112
113            if let ChunkingType::Emitted { .. } = ref_data.chunking_type {
114                emitted_references.insert((ref_data, node));
115            }
116
117            if parent == node {
118                // A self-reference
119                Ok(GraphTraversalAction::Skip)
120            } else {
121                let [Some(parent_membership), Some(current_membership)] =
122                    module_entry_membership.get_disjoint_mut([&parent, &node])
123                else {
124                    // All modules are inserted in the previous iteration
125                    // Technically unreachable, but could be reached due to eventual
126                    // consistency
127                    bail!("Module entry membership not found");
128                };
129
130                if current_membership.is_empty() {
131                    // Initial visit, clone instead of merging
132                    *current_membership = parent_membership.clone();
133                    Ok(GraphTraversalAction::Continue)
134                } else if parent_membership.is_proper_superset(current_membership) {
135                    // Add bits from parent, and continue traversal because changed
136                    **current_membership |= &**parent_membership;
137                    Ok(GraphTraversalAction::Continue)
138                } else {
139                    // Unchanged, no need to forward to children
140                    Ok(GraphTraversalAction::Skip)
141                }
142            }
143        },
144        // Visit modules in reverse postorder (lowest rank first, the traversal visits the highest
145        // priority first), so that most modules are visited once, with their final membership.
146        |successor, _| {
147            Ok(Reverse(
148                *module_rank
149                    .get(&successor)
150                    .context("Module rank not found")?,
151            ))
152        },
153    )?;
154
155    for collecting_module in &collecting_modules {
156        let collecting_module = ResolvedVc::upcast(*collecting_module);
157        let collecting_membership = module_entry_membership
158            .get(&collecting_module)
159            .context("Module entry membership not found")?;
160
161        if collecting_membership.len() > 1 {
162            ModuleIssue::new(
163                *collecting_module.ident().to_resolved().await?,
164                rcstr!("Invalid use of __turbopack_collect__"),
165                rcstr!(
166                    "A module containing __turbopack_collect__ must not be reachable from \
167                     multiple entry chunk groups. Move the call into an entry-specific module."
168                ),
169                None,
170            )
171            .to_resolved()
172            .await?
173            .emit();
174        }
175    }
176
177    let collecting_modules = {
178        let mut map: FxHashMap<RcStr, Vec<ResolvedVc<Box<dyn CollectingModule>>>> =
179            FxHashMap::default();
180        for (m, namespace) in collecting_modules
181            .iter()
182            .map(async |target| Ok((*target, target.namespace().owned().await?)))
183            .try_join()
184            .await?
185        {
186            map.entry(namespace).or_default().push(m);
187        }
188        map
189    };
190
191    // Now we have all necessary information. List out all collected references for each (Entry
192    // Module, Collecting Module) pair they are contained in.
193
194    // Same type as `struct CollectedModules`:
195    // (Collecting Module) -> Vec<(entry_groups index, Vec<(Reference, Collected Module)>)>
196    #[allow(clippy::type_complexity)]
197    let mut collected_references: FxIndexMap<
198        ResolvedVc<Box<dyn Module>>,
199        FxIndexMap<u32, Vec<(RefData, ResolvedVc<Box<dyn Module>>)>>,
200    > = FxIndexMap::default();
201
202    for (ref_data, emitted_module) in emitted_references {
203        let emitted_membership = module_entry_membership
204            .get(&emitted_module)
205            .context("Module entry membership not found")?;
206
207        let ChunkingType::Emitted {
208            namespace,
209            emit_to_all_entries,
210        } = &ref_data.chunking_type
211        else {
212            bail!("unreachable: expected emitted reference");
213        };
214
215        for collecting_module in collecting_modules.get(namespace).into_iter().flatten() {
216            let collecting_membership = module_entry_membership
217                .get(&ResolvedVc::upcast(*collecting_module))
218                .context("Module entry membership not found")?;
219
220            let matching_chunk_groups = if *emit_to_all_entries {
221                // Add to all entry groups the collecting module is in.
222                Cow::Borrowed(&**collecting_membership)
223            } else {
224                // Add to all entry groups the collecting module is in that also contain the emitted
225                // module.
226                Cow::Owned((**collecting_membership).clone() & (&**emitted_membership))
227            };
228            if !matching_chunk_groups.is_empty() {
229                let refs = collected_references
230                    .entry(ResolvedVc::upcast(*collecting_module))
231                    .or_default();
232                for entry in matching_chunk_groups.iter() {
233                    refs.entry(entry).or_default().push((
234                        RefData {
235                            chunking_type: ChunkingType::Collected {
236                                namespace: namespace.clone(),
237                            },
238                            ..ref_data.clone()
239                        },
240                        emitted_module,
241                    ));
242                }
243            }
244        }
245    }
246
247    Ok(CollectedModules(
248        collected_references
249            .into_iter()
250            .map(|(k, v)| {
251                (
252                    k,
253                    v.into_iter()
254                        .map(|(k, v)| (entry_groups[k as usize].clone(), v))
255                        .collect(),
256                )
257            })
258            .collect(),
259    )
260    .cell())
261}