turbopack_core/module_graph/
export_usage.rs

1//! Intermediate tree shaking that uses global information but not good as the full tree shaking.
2
3use anyhow::Result;
4use auto_hash_map::AutoSet;
5use rustc_hash::{FxHashMap, FxHashSet};
6use turbo_rcstr::RcStr;
7use turbo_tasks::{ResolvedVc, Vc};
8
9use crate::{module::Module, module_graph::ModuleGraph, resolve::ExportUsage};
10
11#[turbo_tasks::function(operation)]
12pub async fn compute_export_usage_info(
13    graph: ResolvedVc<ModuleGraph>,
14) -> Result<Vc<ExportUsageInfo>> {
15    let mut used_exports = FxHashMap::<_, ModuleExportUsageInfo>::default();
16    let graph = graph.await?;
17    graph
18        .traverse_all_edges_unordered(|(_, ref_data), target| {
19            let e = used_exports.entry(target.module).or_default();
20
21            e.add(&ref_data.export);
22
23            Ok(())
24        })
25        .await?;
26    // Compute cycles and select modules to be 'circuit breakers'
27    // A circuit breaker module will need to eagerly export lazy getters for its exports to break an
28    // evaluation cycle all other modules can export values after defining them
29    let mut circuit_breakers = FxHashSet::default();
30    graph
31        .traverse_cycles(
32            |e| e.chunking_type.is_parallel(),
33            |cycle| {
34                // To break cycles we need to ensure that no importing module can observe a
35                // partially populated exports object.
36
37                // We could compute this based on the module graph via a DFS from each entry point
38                // to the cycle.  Whatever node is hit first is an entry point to the cycle.
39                // (scope hoisting does something similar) and then we would only need to
40                // mark 'entry' modules (basically the targets of back edges in the export graph) as
41                // circuit breakers.  For now we just mark everything on the theory that cycles are
42                // rare.  For vercel-site on 8/22/2025 there were 106 cycles covering 800 modules
43                // (or 1.2% of all modules).  So with this analysis we could potentially drop 80% of
44                // the cycle breaker modules.
45                circuit_breakers.extend(cycle.iter().map(|n| n.module));
46            },
47        )
48        .await?;
49
50    Ok(ExportUsageInfo {
51        used_exports,
52        circuit_breakers,
53    }
54    .cell())
55}
56
57#[turbo_tasks::value(transparent)]
58pub struct OptionExportUsageInfo(Option<ResolvedVc<ExportUsageInfo>>);
59
60#[turbo_tasks::value]
61pub struct ExportUsageInfo {
62    used_exports: FxHashMap<ResolvedVc<Box<dyn Module>>, ModuleExportUsageInfo>,
63    circuit_breakers: FxHashSet<ResolvedVc<Box<dyn Module>>>,
64}
65
66#[turbo_tasks::value(shared)]
67pub struct ModuleExportUsage {
68    pub export_usage: ResolvedVc<ModuleExportUsageInfo>,
69    // Whether this module exists in an import cycle and has been selected to break the cycle.
70    pub is_circuit_breaker: bool,
71}
72
73#[turbo_tasks::value_impl]
74impl ModuleExportUsage {
75    #[turbo_tasks::function]
76    pub async fn all() -> Result<Vc<Self>> {
77        Ok(Self {
78            export_usage: ModuleExportUsageInfo::all().to_resolved().await?,
79            is_circuit_breaker: true,
80        }
81        .cell())
82    }
83}
84
85impl ExportUsageInfo {
86    pub async fn used_exports(
87        &self,
88        module: ResolvedVc<Box<dyn Module>>,
89    ) -> Result<Vc<ModuleExportUsage>> {
90        let is_circuit_breaker = self.circuit_breakers.contains(&module);
91        let export_usage = if let Some(exports) = self.used_exports.get(&module) {
92            exports.clone().resolved_cell()
93        } else {
94            // We exclude template files from tree shaking because they are entrypoints to the
95            // module graph.
96            ModuleExportUsageInfo::all().to_resolved().await?
97        };
98        Ok(ModuleExportUsage {
99            export_usage,
100            is_circuit_breaker,
101        }
102        .cell())
103    }
104}
105
106#[turbo_tasks::value]
107#[derive(Default, Clone)]
108pub enum ModuleExportUsageInfo {
109    /// Only the side effects are needed, no exports is used.
110    #[default]
111    Evaluation,
112    Exports(AutoSet<RcStr>),
113    All,
114}
115
116#[turbo_tasks::value_impl]
117impl ModuleExportUsageInfo {
118    #[turbo_tasks::function]
119    pub fn all() -> Vc<Self> {
120        ModuleExportUsageInfo::All.cell()
121    }
122}
123
124impl ModuleExportUsageInfo {
125    fn add(&mut self, usage: &ExportUsage) {
126        match (&mut *self, usage) {
127            (Self::All, _) => {}
128            (_, ExportUsage::All) => {
129                *self = Self::All;
130            }
131            (Self::Evaluation, ExportUsage::Named(name)) => {
132                // Promote evaluation to something more specific
133                *self = Self::Exports(AutoSet::from_iter([name.clone()]));
134            }
135
136            (Self::Exports(l), ExportUsage::Named(r)) => {
137                // Merge exports
138                l.insert(r.clone());
139            }
140
141            (_, ExportUsage::Evaluation) => {
142                // Ignore evaluation
143            }
144        }
145    }
146
147    pub fn is_export_used(&self, export: &RcStr) -> bool {
148        match self {
149            Self::All => true,
150            Self::Evaluation => false,
151            Self::Exports(exports) => exports.contains(export),
152        }
153    }
154}