turbopack_ecmascript/references/esm/
export.rs

1use std::{borrow::Cow, collections::BTreeMap, ops::ControlFlow};
2
3use anyhow::Result;
4use rustc_hash::FxHashSet;
5use serde::{Deserialize, Serialize};
6use swc_core::{
7    common::DUMMY_SP,
8    ecma::ast::{
9        AssignTarget, ComputedPropName, Expr, ExprStmt, Ident, KeyValueProp, Lit, MemberExpr,
10        MemberProp, ObjectLit, Prop, PropName, PropOrSpread, SimpleAssignTarget, Stmt, Str,
11    },
12    quote, quote_expr,
13};
14use turbo_rcstr::RcStr;
15use turbo_tasks::{
16    FxIndexMap, FxIndexSet, NonLocalValue, ResolvedVc, TryFlatJoinIterExt, ValueToString, Vc,
17    trace::TraceRawVcs,
18};
19use turbo_tasks_fs::glob::Glob;
20use turbopack_core::{
21    chunk::ChunkingContext,
22    ident::AssetIdent,
23    issue::{IssueExt, IssueSeverity, StyledString, analyze::AnalyzeIssue},
24    module::Module,
25    module_graph::ModuleGraph,
26    reference::ModuleReference,
27    resolve::ModulePart,
28};
29
30use super::base::ReferencedAsset;
31use crate::{
32    EcmascriptModuleAsset,
33    chunk::{EcmascriptChunkPlaceable, EcmascriptExports},
34    code_gen::{CodeGeneration, CodeGenerationHoistedStmt},
35    magic_identifier,
36    parse::ParseResult,
37    runtime_functions::{TURBOPACK_DYNAMIC, TURBOPACK_ESM},
38    tree_shake::asset::EcmascriptModulePartAsset,
39};
40
41#[derive(Clone, Hash, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue)]
42pub enum EsmExport {
43    /// A local binding that is exported (export { a } or export const a = 1)
44    ///
45    /// The last bool is true if the binding is a mutable binding
46    LocalBinding(RcStr, bool),
47    /// An imported binding that is exported (export { a as b } from "...")
48    ///
49    /// The last bool is true if the binding is a mutable binding
50    ImportedBinding(ResolvedVc<Box<dyn ModuleReference>>, RcStr, bool),
51    /// An imported namespace that is exported (export * from "...")
52    ImportedNamespace(ResolvedVc<Box<dyn ModuleReference>>),
53    /// An error occurred while resolving the export
54    Error,
55}
56
57#[turbo_tasks::function]
58pub async fn is_export_missing(
59    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
60    export_name: RcStr,
61) -> Result<Vc<bool>> {
62    if export_name == "__turbopack_module_id__" {
63        return Ok(Vc::cell(false));
64    }
65
66    let exports = module.get_exports().await?;
67    let exports = match &*exports {
68        EcmascriptExports::None => return Ok(Vc::cell(true)),
69        EcmascriptExports::Value => return Ok(Vc::cell(false)),
70        EcmascriptExports::CommonJs => return Ok(Vc::cell(false)),
71        EcmascriptExports::EmptyCommonJs => return Ok(Vc::cell(export_name != "default")),
72        EcmascriptExports::DynamicNamespace => return Ok(Vc::cell(false)),
73        EcmascriptExports::EsmExports(exports) => *exports,
74    };
75
76    let exports = exports.await?;
77    if exports.exports.contains_key(&export_name) {
78        return Ok(Vc::cell(false));
79    }
80    if export_name == "default" {
81        return Ok(Vc::cell(true));
82    }
83
84    if exports.star_exports.is_empty() {
85        return Ok(Vc::cell(true));
86    }
87
88    let all_export_names = get_all_export_names(*module).await?;
89    if all_export_names.esm_exports.contains_key(&export_name) {
90        return Ok(Vc::cell(false));
91    }
92
93    for &dynamic_module in &all_export_names.dynamic_exporting_modules {
94        let exports = dynamic_module.get_exports().await?;
95        match &*exports {
96            EcmascriptExports::Value
97            | EcmascriptExports::CommonJs
98            | EcmascriptExports::DynamicNamespace => {
99                return Ok(Vc::cell(false));
100            }
101            EcmascriptExports::None
102            | EcmascriptExports::EmptyCommonJs
103            | EcmascriptExports::EsmExports(_) => {}
104        }
105    }
106
107    Ok(Vc::cell(true))
108}
109
110#[turbo_tasks::function]
111pub async fn all_known_export_names(
112    module: Vc<Box<dyn EcmascriptChunkPlaceable>>,
113) -> Result<Vc<Vec<RcStr>>> {
114    let export_names = get_all_export_names(module).await?;
115    Ok(Vc::cell(export_names.esm_exports.keys().cloned().collect()))
116}
117
118#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue)]
119pub enum FoundExportType {
120    Found,
121    Dynamic,
122    NotFound,
123    SideEffects,
124    Unknown,
125}
126
127#[turbo_tasks::value]
128pub struct FollowExportsResult {
129    pub module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
130    pub export_name: Option<RcStr>,
131    pub ty: FoundExportType,
132}
133
134#[turbo_tasks::function]
135pub async fn follow_reexports(
136    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
137    export_name: RcStr,
138    side_effect_free_packages: Vc<Glob>,
139    ignore_side_effect_of_entry: bool,
140) -> Result<Vc<FollowExportsResult>> {
141    if !ignore_side_effect_of_entry
142        && !*module
143            .is_marked_as_side_effect_free(side_effect_free_packages)
144            .await?
145    {
146        return Ok(FollowExportsResult::cell(FollowExportsResult {
147            module,
148            export_name: Some(export_name),
149            ty: FoundExportType::SideEffects,
150        }));
151    }
152    let mut module = module;
153    let mut export_name = export_name;
154    loop {
155        let exports = module.get_exports().await?;
156        let EcmascriptExports::EsmExports(exports) = &*exports else {
157            return Ok(FollowExportsResult::cell(FollowExportsResult {
158                module,
159                export_name: Some(export_name),
160                ty: FoundExportType::Dynamic,
161            }));
162        };
163
164        // Try to find the export in the local exports
165        let exports_ref = exports.await?;
166        if let Some(export) = exports_ref.exports.get(&export_name) {
167            match handle_declared_export(module, export_name, export, side_effect_free_packages)
168                .await?
169            {
170                ControlFlow::Continue((m, n)) => {
171                    module = m.to_resolved().await?;
172                    export_name = n;
173                    continue;
174                }
175                ControlFlow::Break(result) => {
176                    return Ok(result.cell());
177                }
178            }
179        }
180
181        // Try to find the export in the star exports
182        if !exports_ref.star_exports.is_empty() && &*export_name != "default" {
183            let result = find_export_from_reexports(*module, export_name.clone()).await?;
184            if let Some(m) = result.esm_export {
185                module = m;
186                continue;
187            }
188            return match &result.dynamic_exporting_modules[..] {
189                [] => Ok(FollowExportsResult {
190                    module,
191                    export_name: Some(export_name),
192                    ty: FoundExportType::NotFound,
193                }
194                .cell()),
195                [module] => Ok(FollowExportsResult {
196                    module: *module,
197                    export_name: Some(export_name),
198                    ty: FoundExportType::Dynamic,
199                }
200                .cell()),
201                _ => Ok(FollowExportsResult {
202                    module,
203                    export_name: Some(export_name),
204                    ty: FoundExportType::Dynamic,
205                }
206                .cell()),
207            };
208        }
209
210        return Ok(FollowExportsResult::cell(FollowExportsResult {
211            module,
212            export_name: Some(export_name),
213            ty: FoundExportType::NotFound,
214        }));
215    }
216}
217
218async fn handle_declared_export(
219    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
220    export_name: RcStr,
221    export: &EsmExport,
222    side_effect_free_packages: Vc<Glob>,
223) -> Result<ControlFlow<FollowExportsResult, (Vc<Box<dyn EcmascriptChunkPlaceable>>, RcStr)>> {
224    match export {
225        EsmExport::ImportedBinding(reference, name, _) => {
226            if let ReferencedAsset::Some(module) =
227                *ReferencedAsset::from_resolve_result(reference.resolve_reference()).await?
228            {
229                if !*module
230                    .is_marked_as_side_effect_free(side_effect_free_packages)
231                    .await?
232                {
233                    return Ok(ControlFlow::Break(FollowExportsResult {
234                        module,
235                        export_name: Some(name.clone()),
236                        ty: FoundExportType::SideEffects,
237                    }));
238                }
239                return Ok(ControlFlow::Continue((*module, name.clone())));
240            }
241        }
242        EsmExport::ImportedNamespace(reference) => {
243            if let ReferencedAsset::Some(module) =
244                *ReferencedAsset::from_resolve_result(reference.resolve_reference()).await?
245            {
246                return Ok(ControlFlow::Break(FollowExportsResult {
247                    module,
248                    export_name: None,
249                    ty: FoundExportType::Found,
250                }));
251            }
252        }
253        EsmExport::LocalBinding(..) => {
254            return Ok(ControlFlow::Break(FollowExportsResult {
255                module,
256                export_name: Some(export_name),
257                ty: FoundExportType::Found,
258            }));
259        }
260        EsmExport::Error => {
261            return Ok(ControlFlow::Break(FollowExportsResult {
262                module,
263                export_name: Some(export_name),
264                ty: FoundExportType::Unknown,
265            }));
266        }
267    }
268    Ok(ControlFlow::Break(FollowExportsResult {
269        module,
270        export_name: Some(export_name),
271        ty: FoundExportType::Unknown,
272    }))
273}
274
275#[turbo_tasks::value]
276struct FindExportFromReexportsResult {
277    esm_export: Option<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
278    dynamic_exporting_modules: Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
279}
280
281#[turbo_tasks::function]
282async fn find_export_from_reexports(
283    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
284    export_name: RcStr,
285) -> Result<Vc<FindExportFromReexportsResult>> {
286    if let Some(module) =
287        Vc::try_resolve_downcast_type::<EcmascriptModulePartAsset>(*module).await?
288    {
289        if matches!(module.await?.part, ModulePart::Exports) {
290            let module_part = EcmascriptModulePartAsset::select_part(
291                *module.await?.full_module,
292                ModulePart::export(export_name.clone()),
293            );
294
295            // If we apply this logic to EcmascriptModuleAsset, we will resolve everything in the
296            // target module.
297            if (Vc::try_resolve_downcast_type::<EcmascriptModuleAsset>(module_part).await?)
298                .is_none()
299            {
300                return Ok(find_export_from_reexports(
301                    Vc::upcast(module_part),
302                    export_name,
303                ));
304            }
305        }
306    }
307
308    let all_export_names = get_all_export_names(*module).await?;
309    let esm_export = all_export_names.esm_exports.get(&export_name).copied();
310    Ok(FindExportFromReexportsResult {
311        esm_export,
312        dynamic_exporting_modules: all_export_names.dynamic_exporting_modules.clone(),
313    }
314    .cell())
315}
316
317#[turbo_tasks::value]
318struct AllExportNamesResult {
319    esm_exports: FxIndexMap<RcStr, ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
320    dynamic_exporting_modules: Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
321}
322
323#[turbo_tasks::function]
324async fn get_all_export_names(
325    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
326) -> Result<Vc<AllExportNamesResult>> {
327    let exports = module.get_exports().await?;
328    let EcmascriptExports::EsmExports(exports) = &*exports else {
329        return Ok(AllExportNamesResult {
330            esm_exports: FxIndexMap::default(),
331            dynamic_exporting_modules: vec![module],
332        }
333        .cell());
334    };
335
336    let exports = exports.await?;
337    let mut esm_exports = FxIndexMap::default();
338    let mut dynamic_exporting_modules = Vec::new();
339    esm_exports.extend(exports.exports.keys().cloned().map(|n| (n, module)));
340    let star_export_names = exports
341        .star_exports
342        .iter()
343        .map(|esm_ref| async {
344            Ok(
345                if let ReferencedAsset::Some(m) =
346                    *ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?
347                {
348                    Some(get_all_export_names(*m))
349                } else {
350                    None
351                },
352            )
353        })
354        .try_flat_join()
355        .await?;
356    for star_export_names in star_export_names {
357        let star_export_names = star_export_names.await?;
358        esm_exports.extend(
359            star_export_names
360                .esm_exports
361                .iter()
362                .map(|(k, &v)| (k.clone(), v)),
363        );
364        dynamic_exporting_modules
365            .extend(star_export_names.dynamic_exporting_modules.iter().copied());
366    }
367
368    Ok(AllExportNamesResult {
369        esm_exports,
370        dynamic_exporting_modules,
371    }
372    .cell())
373}
374
375#[turbo_tasks::value]
376pub struct ExpandStarResult {
377    pub star_exports: Vec<RcStr>,
378    pub has_dynamic_exports: bool,
379}
380
381#[turbo_tasks::function]
382pub async fn expand_star_exports(
383    root_module: Vc<Box<dyn EcmascriptChunkPlaceable>>,
384) -> Result<Vc<ExpandStarResult>> {
385    let mut set = FxIndexSet::default();
386    let mut has_dynamic_exports = false;
387    let mut checked_modules = FxHashSet::default();
388    checked_modules.insert(root_module);
389    let mut queue = vec![(root_module, root_module.get_exports())];
390    while let Some((asset, exports)) = queue.pop() {
391        match &*exports.await? {
392            EcmascriptExports::EsmExports(exports) => {
393                let exports = exports.await?;
394                set.extend(exports.exports.keys().filter(|n| *n != "default").cloned());
395                for esm_ref in exports.star_exports.iter() {
396                    if let ReferencedAsset::Some(asset) =
397                        &*ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?
398                    {
399                        if checked_modules.insert(**asset) {
400                            queue.push((**asset, asset.get_exports()));
401                        }
402                    }
403                }
404            }
405            EcmascriptExports::None | EcmascriptExports::EmptyCommonJs => {
406                emit_star_exports_issue(
407                    asset.ident(),
408                    format!(
409                        "export * used with module {} which has no exports\nTypescript only: Did \
410                         you want to export only types with `export type * from \"...\"`?\nNote: \
411                         Using `export type` is more efficient than `export *` as it won't emit \
412                         any runtime code.",
413                        asset.ident().to_string().await?
414                    )
415                    .into(),
416                )
417                .await?
418            }
419            EcmascriptExports::Value => {
420                emit_star_exports_issue(
421                    asset.ident(),
422                    format!(
423                        "export * used with module {} which only has a default export (default \
424                         export is not exported with export *)\nDid you want to use `export {{ \
425                         default }} from \"...\";` instead?",
426                        asset.ident().to_string().await?
427                    )
428                    .into(),
429                )
430                .await?
431            }
432            EcmascriptExports::CommonJs => {
433                has_dynamic_exports = true;
434                emit_star_exports_issue(
435                    asset.ident(),
436                    format!(
437                        "export * used with module {} which is a CommonJS module with exports \
438                         only available at runtime\nList all export names manually (`export {{ a, \
439                         b, c }} from \"...\") or rewrite the module to ESM, to avoid the \
440                         additional runtime code.`",
441                        asset.ident().to_string().await?
442                    )
443                    .into(),
444                )
445                .await?;
446            }
447            EcmascriptExports::DynamicNamespace => {
448                has_dynamic_exports = true;
449            }
450        }
451    }
452
453    Ok(ExpandStarResult {
454        star_exports: set.into_iter().collect(),
455        has_dynamic_exports,
456    }
457    .cell())
458}
459
460async fn emit_star_exports_issue(source_ident: Vc<AssetIdent>, message: RcStr) -> Result<()> {
461    AnalyzeIssue::new(
462        IssueSeverity::Warning,
463        source_ident,
464        Vc::cell("unexpected export *".into()),
465        StyledString::Text(message).cell(),
466        None,
467        None,
468    )
469    .to_resolved()
470    .await?
471    .emit();
472    Ok(())
473}
474
475#[turbo_tasks::value(shared)]
476#[derive(Hash, Debug)]
477pub struct EsmExports {
478    pub exports: BTreeMap<RcStr, EsmExport>,
479    pub star_exports: Vec<ResolvedVc<Box<dyn ModuleReference>>>,
480}
481
482/// The expanded version of [`EsmExports`], the `exports` field here includes all exports that could
483/// be expanded from `star_exports`.
484///
485/// [`EsmExports::star_exports`] that could not be (fully) expanded end up in `dynamic_exports`.
486#[turbo_tasks::value(shared)]
487#[derive(Hash, Debug)]
488pub struct ExpandedExports {
489    pub exports: BTreeMap<RcStr, EsmExport>,
490    /// Modules we couldn't analyse all exports of.
491    pub dynamic_exports: Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
492}
493
494#[turbo_tasks::value_impl]
495impl EsmExports {
496    #[turbo_tasks::function]
497    pub async fn expand_exports(&self) -> Result<Vc<ExpandedExports>> {
498        let mut exports: BTreeMap<RcStr, EsmExport> = self.exports.clone();
499        let mut dynamic_exports = vec![];
500
501        for &esm_ref in self.star_exports.iter() {
502            // TODO(PACK-2176): we probably need to handle re-exporting from external
503            // modules.
504            let ReferencedAsset::Some(asset) =
505                &*ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?
506            else {
507                continue;
508            };
509
510            let export_info = expand_star_exports(**asset).await?;
511
512            for export in &export_info.star_exports {
513                if !exports.contains_key(export) {
514                    exports.insert(
515                        export.clone(),
516                        EsmExport::ImportedBinding(
517                            ResolvedVc::upcast(esm_ref),
518                            export.clone(),
519                            false,
520                        ),
521                    );
522                }
523            }
524
525            if export_info.has_dynamic_exports {
526                dynamic_exports.push(*asset);
527            }
528        }
529
530        Ok(ExpandedExports {
531            exports,
532            dynamic_exports,
533        }
534        .cell())
535    }
536}
537
538impl EsmExports {
539    pub async fn code_generation(
540        self: Vc<Self>,
541        _module_graph: Vc<ModuleGraph>,
542        chunking_context: Vc<Box<dyn ChunkingContext>>,
543        parsed: Option<Vc<ParseResult>>,
544    ) -> Result<CodeGeneration> {
545        let expanded = self.expand_exports().await?;
546        let parsed = if let Some(parsed) = parsed {
547            Some(parsed.await?)
548        } else {
549            None
550        };
551
552        let mut dynamic_exports = Vec::<Box<Expr>>::new();
553        for dynamic_export_asset in &expanded.dynamic_exports {
554            let ident =
555                ReferencedAsset::get_ident_from_placeable(dynamic_export_asset, chunking_context)
556                    .await?;
557
558            dynamic_exports.push(quote_expr!(
559                "$turbopack_dynamic($arg)",
560                turbopack_dynamic: Expr = TURBOPACK_DYNAMIC.into(),
561                arg: Expr = Ident::new(ident.into(), DUMMY_SP, Default::default()).into()
562            ));
563        }
564
565        let mut props = Vec::new();
566        for (exported, local) in &expanded.exports {
567            let expr = match local {
568                EsmExport::Error => Some(quote!(
569                    "(() => { throw new Error(\"Failed binding. See build errors!\"); })" as Expr,
570                )),
571                EsmExport::LocalBinding(name, mutable) => {
572                    let local = if name == "default" {
573                        Cow::Owned(magic_identifier::mangle("default export"))
574                    } else {
575                        Cow::Borrowed(name.as_str())
576                    };
577                    let ctxt = parsed
578                        .as_ref()
579                        .and_then(|parsed| {
580                            if let ParseResult::Ok { eval_context, .. } = &**parsed {
581                                eval_context.imports.exports.get(name).map(|id| id.1)
582                            } else {
583                                None
584                            }
585                        })
586                        .unwrap_or_default();
587
588                    if *mutable {
589                        Some(quote!(
590                            "([() => $local, ($new) => $local = $new])" as Expr,
591                            local = Ident::new(local.into(), DUMMY_SP, ctxt),
592                            new = Ident::new(format!("new_{name}").into(), DUMMY_SP, ctxt),
593                        ))
594                    } else {
595                        Some(quote!(
596                            "(() => $local)" as Expr,
597                            local = Ident::new((name as &str).into(), DUMMY_SP, ctxt)
598                        ))
599                    }
600                }
601                EsmExport::ImportedBinding(esm_ref, name, mutable) => {
602                    let referenced_asset =
603                        ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?;
604                    referenced_asset.get_ident(
605                        chunking_context
606                    ).await?.map(|ident| {
607                        let expr = MemberExpr {
608                            span: DUMMY_SP,
609                            obj: Box::new(Expr::Ident(Ident::new(
610                                ident.into(),
611                                DUMMY_SP,
612                                Default::default(),
613                            ))),
614                            prop: MemberProp::Computed(ComputedPropName {
615                                span: DUMMY_SP,
616                                expr: Box::new(Expr::Lit(Lit::Str(Str {
617                                    span: DUMMY_SP,
618                                    value: (name as &str).into(),
619                                    raw: None,
620                                }))),
621                            }),
622                        };
623                        if *mutable {
624                            quote!(
625                                "([() => $expr, ($new) => $lhs = $new])" as Expr,
626                                expr: Expr = Expr::Member(expr.clone()),
627                                lhs: AssignTarget = AssignTarget::Simple(SimpleAssignTarget::Member(expr)),
628                                new = Ident::new(
629                                    format!("new_{name}").into(),
630                                    DUMMY_SP,
631                                    Default::default()
632                                ),
633                            )
634                        } else {
635                            quote!(
636                                "(() => $expr)" as Expr,
637                                expr: Expr = Expr::Member(expr),
638                            )
639                        }
640                    })
641                }
642                EsmExport::ImportedNamespace(esm_ref) => {
643                    let referenced_asset =
644                        ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?;
645                    referenced_asset
646                        .get_ident(chunking_context)
647                        .await?
648                        .map(|ident| {
649                            quote!(
650                                "(() => $imported)" as Expr,
651                                imported = Ident::new(ident.into(), DUMMY_SP, Default::default())
652                            )
653                        })
654                }
655            };
656            if let Some(expr) = expr {
657                props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
658                    key: PropName::Str(Str {
659                        span: DUMMY_SP,
660                        value: exported.as_str().into(),
661                        raw: None,
662                    }),
663                    value: Box::new(expr),
664                }))));
665            }
666        }
667        let getters = Expr::Object(ObjectLit {
668            span: DUMMY_SP,
669            props,
670        });
671        let dynamic_stmt = if !dynamic_exports.is_empty() {
672            Some(Stmt::Expr(ExprStmt {
673                span: DUMMY_SP,
674                expr: Expr::from_exprs(dynamic_exports),
675            }))
676        } else {
677            None
678        };
679
680        Ok(CodeGeneration::new(
681            vec![],
682            [dynamic_stmt
683                .map(|stmt| CodeGenerationHoistedStmt::new("__turbopack_dynamic__".into(), stmt))]
684            .into_iter()
685            .flatten()
686            .collect(),
687            vec![CodeGenerationHoistedStmt::new(
688                "__turbopack_esm__".into(),
689                quote!("$turbopack_esm($getters);" as Stmt,
690                    turbopack_esm: Expr = TURBOPACK_ESM.into(),
691                    getters: Expr = getters
692                ),
693            )],
694        ))
695    }
696}