Skip to main content

turbopack_ecmascript/references/
exports.rs

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