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            .resolved_cell();
146
147            import_references.push(reference);
148            if should_add_evaluation {
149                esm_evaluation_reference_idxs.push(i);
150            }
151        }
152        anyhow::Ok(import_references)
153    }
154    .instrument(span)
155    .await?;
156
157    let span = tracing::trace_span!("exports");
158    let exports = async {
159        let esm_star_exports: Vec<ResolvedVc<Box<dyn ModuleReference>>> = eval_context
160            .imports
161            .reexport_namespaces()
162            .map(|i| ResolvedVc::upcast(import_references[i]))
163            .collect();
164        let esm_exports = eval_context
165            .imports
166            .as_esm_exports(&import_references, eval_context)?;
167
168        for idx in eval_context.imports.reexports_reference_idxs() {
169            esm_reexport_reference_idxs.push(idx);
170        }
171
172        anyhow::Ok(
173            if !esm_exports.is_empty() || !esm_star_exports.is_empty() {
174                if specified_type == SpecifiedModuleType::CommonJs {
175                    SpecifiedModuleTypeIssue {
176                        // TODO(PACK-4879): this should point at one of the exports
177                        source: IssueSource::from_source_only(source),
178                        specified_type,
179                    }
180                    .resolved_cell()
181                    .emit();
182                }
183
184                let esm_exports = EsmExports {
185                    exports: esm_exports,
186                    star_exports: esm_star_exports,
187                }
188                .cell();
189
190                EcmascriptExports::EsmExports(esm_exports.to_resolved().await?)
191            } else if specified_type == SpecifiedModuleType::EcmaScript {
192                match detect_dynamic_export(program) {
193                    DetectedDynamicExportType::CommonJs => {
194                        SpecifiedModuleTypeIssue {
195                            // TODO(PACK-4879): this should point at the source location of the
196                            // commonjs export
197                            source: IssueSource::from_source_only(source),
198                            specified_type,
199                        }
200                        .resolved_cell()
201                        .emit();
202
203                        EcmascriptExports::EsmExports(
204                            EsmExports {
205                                exports: Default::default(),
206                                star_exports: Default::default(),
207                            }
208                            .resolved_cell(),
209                        )
210                    }
211                    DetectedDynamicExportType::Namespace => EcmascriptExports::DynamicNamespace,
212                    DetectedDynamicExportType::Value => EcmascriptExports::Value,
213                    DetectedDynamicExportType::UsingModuleDeclarations
214                    | DetectedDynamicExportType::None => EcmascriptExports::EsmExports(
215                        EsmExports {
216                            exports: Default::default(),
217                            star_exports: Default::default(),
218                        }
219                        .resolved_cell(),
220                    ),
221                }
222            } else {
223                match detect_dynamic_export(program) {
224                    DetectedDynamicExportType::CommonJs => EcmascriptExports::CommonJs,
225                    DetectedDynamicExportType::Namespace => EcmascriptExports::DynamicNamespace,
226                    DetectedDynamicExportType::Value => EcmascriptExports::Value,
227                    DetectedDynamicExportType::UsingModuleDeclarations => {
228                        EcmascriptExports::EsmExports(
229                            EsmExports {
230                                exports: Default::default(),
231                                star_exports: Default::default(),
232                            }
233                            .resolved_cell(),
234                        )
235                    }
236                    DetectedDynamicExportType::None => EcmascriptExports::EmptyCommonJs,
237                }
238            }
239            .resolved_cell(),
240        )
241    }
242    .instrument(span)
243    .await?;
244
245    Ok(EcmascriptExportsAnalysis {
246        exports,
247        import_references: import_references.into_boxed_slice(),
248        esm_reexport_reference_idxs: esm_reexport_reference_idxs.into_boxed_slice(),
249        esm_evaluation_reference_idxs: esm_evaluation_reference_idxs.into_boxed_slice(),
250    }
251    .cell())
252}
253
254#[derive(Debug)]
255enum DetectedDynamicExportType {
256    CommonJs,
257    Namespace,
258    Value,
259    None,
260    UsingModuleDeclarations,
261}
262
263// TODO move into ImportMap
264fn detect_dynamic_export(p: &Program) -> DetectedDynamicExportType {
265    use swc_core::ecma::visit::{Visit, VisitWith, visit_obj_and_computed};
266
267    if let Program::Module(m) = p {
268        // Check for imports/exports
269        if m.body.iter().any(|item| {
270            item.as_module_decl().is_some_and(|module_decl| {
271                module_decl.as_import().is_none_or(|import| {
272                    !is_turbopack_helper_import(import) && !is_swc_helper_import(import)
273                })
274            })
275        }) {
276            return DetectedDynamicExportType::UsingModuleDeclarations;
277        }
278    }
279
280    struct Visitor {
281        cjs: bool,
282        value: bool,
283        namespace: bool,
284        found: bool,
285    }
286
287    impl Visit for Visitor {
288        visit_obj_and_computed!();
289
290        fn visit_ident(&mut self, i: &Ident) {
291            // The detection is not perfect, it might have some false positives, e. g. in
292            // cases where `module` is used in some other way. e. g. `const module = 42;`.
293            // But a false positive doesn't break anything, it only opts out of some
294            // optimizations, which is acceptable.
295            if &*i.sym == "module" || &*i.sym == "exports" {
296                self.cjs = true;
297                self.found = true;
298            }
299            if &*i.sym == "__turbopack_export_value__" {
300                self.value = true;
301                self.found = true;
302            }
303            if &*i.sym == "__turbopack_export_namespace__" {
304                self.namespace = true;
305                self.found = true;
306            }
307        }
308
309        fn visit_expr(&mut self, n: &Expr) {
310            if self.found {
311                return;
312            }
313
314            if let Expr::Member(member) = n
315                && member.obj.is_ident_ref_to("__turbopack_context__")
316                && let MemberProp::Ident(prop) = &member.prop
317            {
318                const TURBOPACK_EXPORT_VALUE_SHORTCUT: &str = TURBOPACK_EXPORT_VALUE.shortcut;
319                const TURBOPACK_EXPORT_NAMESPACE_SHORTCUT: &str =
320                    TURBOPACK_EXPORT_NAMESPACE.shortcut;
321                match &*prop.sym {
322                    TURBOPACK_EXPORT_VALUE_SHORTCUT => {
323                        self.value = true;
324                        self.found = true;
325                    }
326                    TURBOPACK_EXPORT_NAMESPACE_SHORTCUT => {
327                        self.namespace = true;
328                        self.found = true;
329                    }
330                    _ => {}
331                }
332            }
333
334            n.visit_children_with(self);
335        }
336
337        fn visit_stmt(&mut self, n: &Stmt) {
338            if self.found {
339                return;
340            }
341            n.visit_children_with(self);
342        }
343    }
344
345    let mut v = Visitor {
346        cjs: false,
347        value: false,
348        namespace: false,
349        found: false,
350    };
351    p.visit_with(&mut v);
352    if v.cjs {
353        DetectedDynamicExportType::CommonJs
354    } else if v.value {
355        DetectedDynamicExportType::Value
356    } else if v.namespace {
357        DetectedDynamicExportType::Namespace
358    } else {
359        DetectedDynamicExportType::None
360    }
361}
362
363pub fn is_turbopack_helper_import(import: &ImportDecl) -> bool {
364    let annotations = ImportAnnotations::parse(import.with.as_deref());
365
366    annotations.is_some_and(|a| a.get(&TURBOPACK_HELPER_WTF8).is_some())
367}
368
369pub fn is_swc_helper_import(import: &ImportDecl) -> bool {
370    import.src.value.starts_with("@swc/helpers/")
371}