Skip to main content

turbopack_ecmascript/references/
exports.rs

1use anyhow::{Result, bail};
2use swc_core::{
3    common::source_map::SmallPos,
4    ecma::ast::{Expr, Ident, ImportDecl, MemberProp, Program, Stmt},
5};
6use tracing::Instrument;
7use turbo_rcstr::RcStr;
8use turbo_tasks::{ResolvedVc, Vc};
9use turbopack_core::{
10    issue::{IssueExt, IssueSource},
11    reference::ModuleReference,
12    resolve::ModulePart,
13};
14
15use crate::{
16    EcmascriptModuleAsset, EcmascriptParsable, ModuleTypeResult, SpecifiedModuleType,
17    TreeShakingMode,
18    analyzer::imports::{ImportAnnotations, ImportedSymbol},
19    chunk::EcmascriptExports,
20    parse::ParseResult,
21    references::{
22        TURBOPACK_HELPER_WTF8,
23        esm::{EsmAssetReference, EsmExports},
24        type_issue::SpecifiedModuleTypeIssue,
25    },
26    runtime_functions::{TURBOPACK_EXPORT_NAMESPACE, TURBOPACK_EXPORT_VALUE},
27    tree_shake::{part_of_module, split_module},
28};
29
30#[turbo_tasks::value]
31pub struct EcmascriptExportsAnalysis {
32    pub exports: ResolvedVc<EcmascriptExports>,
33    pub import_references: Box<[ResolvedVc<EsmAssetReference>]>,
34    pub esm_reexport_reference_idxs: Box<[usize]>,
35    pub esm_evaluation_reference_idxs: Box<[usize]>,
36}
37
38#[turbo_tasks::function]
39pub async fn compute_ecmascript_module_exports(
40    module: ResolvedVc<EcmascriptModuleAsset>,
41    part: Option<ModulePart>,
42) -> Result<Vc<EcmascriptExportsAnalysis>> {
43    let raw_module = module.await?;
44    let source = raw_module.source;
45    let options = raw_module.options.await?;
46    let import_externals = options.import_externals;
47
48    let parsed = if let Some(part) = part {
49        let split_data = split_module(*module);
50        part_of_module(split_data, part)
51    } else {
52        module.failsafe_parse()
53    };
54
55    let parsed = parsed.await?;
56    let ParseResult::Ok {
57        program,
58        eval_context,
59        ..
60    } = &*parsed
61    else {
62        return Ok(EcmascriptExportsAnalysis {
63            exports: EcmascriptExports::Unknown.resolved_cell(),
64            import_references: Box::new([]),
65            esm_reexport_reference_idxs: Box::new([]),
66            esm_evaluation_reference_idxs: Box::new([]),
67        }
68        .cell());
69    };
70
71    let ModuleTypeResult {
72        module_type: specified_type,
73        ..
74    } = *module.determine_module_type().await?;
75
76    let inner_assets = if let Some(assets) = raw_module.inner_assets {
77        Some(assets.await?)
78    } else {
79        None
80    };
81
82    let mut esm_reexport_reference_idxs: Vec<usize> = vec![];
83    let mut esm_evaluation_reference_idxs: Vec<usize> = vec![];
84
85    let span = tracing::trace_span!("esm import references");
86    let import_references = async {
87        let mut import_references = Vec::with_capacity(eval_context.imports.references().len());
88        for (i, r) in eval_context.imports.references().enumerate() {
89            let mut should_add_evaluation = false;
90
91            let resolve_override = if let Some(inner_assets) = &inner_assets
92                && let Some(req) = r.module_path.as_str()
93                && let Some(a) = inner_assets.get(req)
94            {
95                Some(*a)
96            } else {
97                None
98            };
99
100            let reference = EsmAssetReference::new(
101                module,
102                ResolvedVc::upcast(module),
103                RcStr::from(&*r.module_path.to_string_lossy()),
104                IssueSource::from_swc_offsets(source, r.span.lo.to_u32(), r.span.hi.to_u32()),
105                r.annotations.as_ref().map(|a| (**a).clone()),
106                match &r.imported_symbol {
107                    &ImportedSymbol::ModuleEvaluation => {
108                        should_add_evaluation = true;
109                        Some(ModulePart::evaluation())
110                    }
111                    ImportedSymbol::Symbol(name) => Some(ModulePart::export((&**name).into())),
112                    ImportedSymbol::PartEvaluation(part_id) | ImportedSymbol::Part(part_id) => {
113                        if !matches!(
114                            options.tree_shaking_mode,
115                            Some(TreeShakingMode::ModuleFragments)
116                        ) {
117                            bail!(
118                                "Internal imports only exist in reexports only mode when \
119                                 importing {:?} from {}",
120                                r.imported_symbol,
121                                r.module_path.to_string_lossy()
122                            );
123                        }
124                        if matches!(&r.imported_symbol, ImportedSymbol::PartEvaluation(_)) {
125                            should_add_evaluation = true;
126                        }
127                        Some(ModulePart::internal(*part_id))
128                    }
129                    ImportedSymbol::Exports => matches!(
130                        options.tree_shaking_mode,
131                        Some(TreeShakingMode::ModuleFragments)
132                    )
133                    .then(ModulePart::exports),
134                },
135                eval_context
136                    .imports
137                    .import_usage
138                    .get(&i)
139                    .cloned()
140                    .unwrap_or_default(),
141                import_externals,
142                options.tree_shaking_mode,
143                resolve_override,
144            )
145            .await?
146            .resolved_cell();
147
148            import_references.push(reference);
149            if should_add_evaluation {
150                esm_evaluation_reference_idxs.push(i);
151            }
152        }
153        anyhow::Ok(import_references)
154    }
155    .instrument(span)
156    .await?;
157
158    let span = tracing::trace_span!("exports");
159    let exports = async {
160        let esm_star_exports: Vec<ResolvedVc<Box<dyn ModuleReference>>> = eval_context
161            .imports
162            .reexport_namespaces()
163            .map(|i| ResolvedVc::upcast(import_references[i]))
164            .collect();
165        let esm_exports = eval_context
166            .imports
167            .as_esm_exports(&import_references, eval_context)?;
168
169        for idx in eval_context.imports.reexports_reference_idxs() {
170            esm_reexport_reference_idxs.push(idx);
171        }
172
173        anyhow::Ok(
174            if !esm_exports.is_empty() || !esm_star_exports.is_empty() {
175                if specified_type == SpecifiedModuleType::CommonJs {
176                    SpecifiedModuleTypeIssue {
177                        // TODO(PACK-4879): this should point at one of the exports
178                        source: IssueSource::from_source_only(source),
179                        specified_type,
180                    }
181                    .resolved_cell()
182                    .emit();
183                }
184
185                let esm_exports = EsmExports {
186                    exports: esm_exports,
187                    star_exports: esm_star_exports,
188                }
189                .cell();
190
191                EcmascriptExports::EsmExports(esm_exports.to_resolved().await?)
192            } else if specified_type == SpecifiedModuleType::EcmaScript {
193                match detect_dynamic_export(program) {
194                    DetectedDynamicExportType::CommonJs => {
195                        SpecifiedModuleTypeIssue {
196                            // TODO(PACK-4879): this should point at the source location of the
197                            // commonjs export
198                            source: IssueSource::from_source_only(source),
199                            specified_type,
200                        }
201                        .resolved_cell()
202                        .emit();
203
204                        EcmascriptExports::EsmExports(
205                            EsmExports {
206                                exports: Default::default(),
207                                star_exports: Default::default(),
208                            }
209                            .resolved_cell(),
210                        )
211                    }
212                    DetectedDynamicExportType::Namespace => EcmascriptExports::DynamicNamespace,
213                    DetectedDynamicExportType::Value => EcmascriptExports::Value,
214                    DetectedDynamicExportType::UsingModuleDeclarations
215                    | DetectedDynamicExportType::None => EcmascriptExports::EsmExports(
216                        EsmExports {
217                            exports: Default::default(),
218                            star_exports: Default::default(),
219                        }
220                        .resolved_cell(),
221                    ),
222                }
223            } else {
224                match detect_dynamic_export(program) {
225                    DetectedDynamicExportType::CommonJs => EcmascriptExports::CommonJs,
226                    DetectedDynamicExportType::Namespace => EcmascriptExports::DynamicNamespace,
227                    DetectedDynamicExportType::Value => EcmascriptExports::Value,
228                    DetectedDynamicExportType::UsingModuleDeclarations => {
229                        EcmascriptExports::EsmExports(
230                            EsmExports {
231                                exports: Default::default(),
232                                star_exports: Default::default(),
233                            }
234                            .resolved_cell(),
235                        )
236                    }
237                    DetectedDynamicExportType::None => EcmascriptExports::EmptyCommonJs,
238                }
239            }
240            .resolved_cell(),
241        )
242    }
243    .instrument(span)
244    .await?;
245
246    Ok(EcmascriptExportsAnalysis {
247        exports,
248        import_references: import_references.into_boxed_slice(),
249        esm_reexport_reference_idxs: esm_reexport_reference_idxs.into_boxed_slice(),
250        esm_evaluation_reference_idxs: esm_evaluation_reference_idxs.into_boxed_slice(),
251    }
252    .cell())
253}
254
255#[derive(Debug)]
256enum DetectedDynamicExportType {
257    CommonJs,
258    Namespace,
259    Value,
260    None,
261    UsingModuleDeclarations,
262}
263
264// TODO move into ImportMap
265fn detect_dynamic_export(p: &Program) -> DetectedDynamicExportType {
266    use swc_core::ecma::visit::{Visit, VisitWith, visit_obj_and_computed};
267
268    if let Program::Module(m) = p {
269        // Check for imports/exports
270        if m.body.iter().any(|item| {
271            item.as_module_decl().is_some_and(|module_decl| {
272                module_decl.as_import().is_none_or(|import| {
273                    !is_turbopack_helper_import(import) && !is_swc_helper_import(import)
274                })
275            })
276        }) {
277            return DetectedDynamicExportType::UsingModuleDeclarations;
278        }
279    }
280
281    struct Visitor {
282        cjs: bool,
283        value: bool,
284        namespace: bool,
285        found: bool,
286    }
287
288    impl Visit for Visitor {
289        visit_obj_and_computed!();
290
291        fn visit_ident(&mut self, i: &Ident) {
292            // The detection is not perfect, it might have some false positives, e. g. in
293            // cases where `module` is used in some other way. e. g. `const module = 42;`.
294            // But a false positive doesn't break anything, it only opts out of some
295            // optimizations, which is acceptable.
296            if &*i.sym == "module" || &*i.sym == "exports" {
297                self.cjs = true;
298                self.found = true;
299            }
300            if &*i.sym == "__turbopack_export_value__" {
301                self.value = true;
302                self.found = true;
303            }
304            if &*i.sym == "__turbopack_export_namespace__" {
305                self.namespace = true;
306                self.found = true;
307            }
308        }
309
310        fn visit_expr(&mut self, n: &Expr) {
311            if self.found {
312                return;
313            }
314
315            if let Expr::Member(member) = n
316                && member.obj.is_ident_ref_to("__turbopack_context__")
317                && let MemberProp::Ident(prop) = &member.prop
318            {
319                const TURBOPACK_EXPORT_VALUE_SHORTCUT: &str = TURBOPACK_EXPORT_VALUE.shortcut;
320                const TURBOPACK_EXPORT_NAMESPACE_SHORTCUT: &str =
321                    TURBOPACK_EXPORT_NAMESPACE.shortcut;
322                match &*prop.sym {
323                    TURBOPACK_EXPORT_VALUE_SHORTCUT => {
324                        self.value = true;
325                        self.found = true;
326                    }
327                    TURBOPACK_EXPORT_NAMESPACE_SHORTCUT => {
328                        self.namespace = true;
329                        self.found = true;
330                    }
331                    _ => {}
332                }
333            }
334
335            n.visit_children_with(self);
336        }
337
338        fn visit_stmt(&mut self, n: &Stmt) {
339            if self.found {
340                return;
341            }
342            n.visit_children_with(self);
343        }
344    }
345
346    let mut v = Visitor {
347        cjs: false,
348        value: false,
349        namespace: false,
350        found: false,
351    };
352    p.visit_with(&mut v);
353    if v.cjs {
354        DetectedDynamicExportType::CommonJs
355    } else if v.value {
356        DetectedDynamicExportType::Value
357    } else if v.namespace {
358        DetectedDynamicExportType::Namespace
359    } else {
360        DetectedDynamicExportType::None
361    }
362}
363
364pub fn is_turbopack_helper_import(import: &ImportDecl) -> bool {
365    let annotations = ImportAnnotations::parse(import.with.as_deref());
366
367    annotations.is_some_and(|a| a.get(&TURBOPACK_HELPER_WTF8).is_some())
368}
369
370pub fn is_swc_helper_import(import: &ImportDecl) -> bool {
371    import.src.value.starts_with("@swc/helpers/")
372}