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