Skip to main content

turbopack_ecmascript/references/esm/
export.rs

1use std::{collections::BTreeMap, ops::ControlFlow};
2
3use anyhow::{Result, bail};
4use bincode::{Decode, Encode};
5use indexmap::map::Entry;
6use rustc_hash::FxHashSet;
7use swc_core::{
8    common::{DUMMY_SP, SyntaxContext},
9    ecma::ast::{
10        ArrayLit, AssignTarget, Expr, ExprStmt, Ident, Lit, Number, SimpleAssignTarget, Stmt, Str,
11    },
12    quote, quote_expr,
13};
14use turbo_frozenmap::FrozenMap;
15use turbo_rcstr::{RcStr, rcstr};
16use turbo_tasks::{
17    FxIndexMap, NonLocalValue, ResolvedVc, TryFlatJoinIterExt, Vc, trace::TraceRawVcs, turbofmt,
18};
19use turbopack_core::{
20    chunk::{ChunkingContext, ModuleChunkItemIdExt},
21    ident::AssetIdent,
22    issue::{IssueExt, IssueSeverity, StyledString, analyze::AnalyzeIssue},
23    module::{Module, ModuleSideEffects},
24    module_graph::binding_usage_info::ModuleExportUsageInfo,
25    reference::ModuleReference,
26    resolve::ModulePart,
27};
28
29use crate::{
30    EcmascriptModuleAsset, ScopeHoistingContext,
31    analyzer::graph::EvalContext,
32    chunk::{EcmascriptChunkPlaceable, EcmascriptExports},
33    code_gen::{CodeGeneration, CodeGenerationHoistedStmt},
34    magic_identifier::MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM,
35    module_fragments::part::module::EcmascriptModulePartAsset,
36    references::esm::base::ReferencedAsset,
37    rename::module::EcmascriptModuleRenameModule,
38    runtime_functions::{TURBOPACK_DYNAMIC, TURBOPACK_ESM},
39    utils::module_id_to_lit,
40};
41
42/// Models the 'liveness' of an esm export
43/// All ESM exports are technically live but many never change and we can optimize representation to
44/// support that, this enum tracks the actual behavior of the export binding.
45#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
46pub enum Liveness {
47    // The binding never changes after module evaluation
48    Constant,
49    // The binding may change after module evaluation
50    Live,
51    // The binding needs to be exposed as mutable to callers.  This isn't part of the spec but is
52    // part of our module-fragments optimization where we split modules into parts and preserve
53    // mutability of variables via mutable exports.
54    Mutable,
55}
56
57#[derive(Clone, Hash, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
58pub enum EsmExport {
59    /// A local binding that is exported (export { a } or export const a = 1)
60    ///
61    /// Fields: (local_name, liveness)
62    LocalBinding(RcStr, Liveness),
63    /// An imported binding that is exported (export { a as b } from "...")
64    ///
65    /// Fields: (module_reference, name, is_mutable)
66    ImportedBinding(ResolvedVc<Box<dyn ModuleReference>>, RcStr, bool),
67    /// An imported namespace that is exported (export * from "...")
68    ImportedNamespace(ResolvedVc<Box<dyn ModuleReference>>),
69    /// An error occurred while resolving the export
70    Error,
71}
72
73#[turbo_tasks::function]
74pub async fn is_export_missing(
75    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
76    export_name: RcStr,
77) -> Result<Vc<bool>> {
78    if export_name == "__turbopack_module_id__" {
79        return Ok(Vc::cell(false));
80    }
81
82    let exports = module.get_exports().await?;
83    let exports = match &*exports {
84        EcmascriptExports::None => return Ok(Vc::cell(true)),
85        EcmascriptExports::Unknown => return Ok(Vc::cell(false)),
86        EcmascriptExports::Value => return Ok(Vc::cell(false)),
87        EcmascriptExports::CommonJs => return Ok(Vc::cell(false)),
88        EcmascriptExports::EmptyCommonJs => return Ok(Vc::cell(export_name != "default")),
89        EcmascriptExports::DynamicNamespace => return Ok(Vc::cell(false)),
90        EcmascriptExports::EsmExports(exports) => *exports,
91    };
92
93    let exports = exports.await?;
94    if exports.exports.contains_key(&export_name) {
95        return Ok(Vc::cell(false));
96    }
97    if export_name == "default" {
98        return Ok(Vc::cell(true));
99    }
100
101    if exports.star_exports.is_empty() {
102        return Ok(Vc::cell(true));
103    }
104
105    let all_export_names = get_all_export_names(*module).await?;
106    if all_export_names.esm_exports.contains_key(&export_name) {
107        return Ok(Vc::cell(false));
108    }
109
110    for &dynamic_module in &all_export_names.dynamic_exporting_modules {
111        let exports = dynamic_module.get_exports().await?;
112        match &*exports {
113            EcmascriptExports::Value
114            | EcmascriptExports::CommonJs
115            | EcmascriptExports::DynamicNamespace
116            | EcmascriptExports::Unknown => {
117                return Ok(Vc::cell(false));
118            }
119            EcmascriptExports::None
120            | EcmascriptExports::EmptyCommonJs
121            | EcmascriptExports::EsmExports(_) => {}
122        }
123    }
124
125    Ok(Vc::cell(true))
126}
127
128#[turbo_tasks::function]
129pub async fn all_known_export_names(
130    module: Vc<Box<dyn EcmascriptChunkPlaceable>>,
131) -> Result<Vc<Vec<RcStr>>> {
132    let export_names = get_all_export_names(module).await?;
133    Ok(Vc::cell(export_names.esm_exports.keys().cloned().collect()))
134}
135
136#[derive(Copy, Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
137pub enum FoundExportType {
138    Found,
139    Dynamic,
140    NotFound,
141    SideEffects,
142    Unknown,
143}
144
145#[turbo_tasks::value]
146pub struct FollowExportsResult {
147    pub module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
148    pub export_name: Option<RcStr>,
149    pub ty: FoundExportType,
150}
151
152#[turbo_tasks::function]
153pub async fn follow_reexports(
154    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
155    export_name: RcStr,
156    ignore_side_effect_of_entry: bool,
157) -> Result<Vc<FollowExportsResult>> {
158    let mut ignore_side_effects = ignore_side_effect_of_entry;
159
160    let mut module = module;
161    let mut export_name = export_name;
162    loop {
163        if !ignore_side_effects
164            && *module.side_effects().await? != ModuleSideEffects::SideEffectFree
165        {
166            // TODO It's unfortunate that we have to use the whole module here.
167            // This is often the Facade module, which includes all reexports.
168            // Often we could use Locals + the followed reexports instead.
169            return Ok(FollowExportsResult::cell(FollowExportsResult {
170                module,
171                export_name: Some(export_name),
172                ty: FoundExportType::SideEffects,
173            }));
174        }
175        ignore_side_effects = false;
176
177        let exports = module.get_exports().await?;
178        let EcmascriptExports::EsmExports(exports) = &*exports else {
179            return Ok(FollowExportsResult::cell(FollowExportsResult {
180                module,
181                export_name: Some(export_name),
182                ty: FoundExportType::Dynamic,
183            }));
184        };
185
186        // Try to find the export in the local exports
187        let exports_ref = exports.await?;
188        if let Some(export) = exports_ref.exports.get(&export_name) {
189            match handle_declared_export(module, export_name, export).await? {
190                ControlFlow::Continue((m, n)) => {
191                    module = m.to_resolved().await?;
192                    export_name = n;
193                    continue;
194                }
195                ControlFlow::Break(result) => {
196                    return Ok(result.cell());
197                }
198            }
199        }
200
201        // Try to find the export in the star exports
202        if !exports_ref.star_exports.is_empty() && &*export_name != "default" {
203            let result = find_export_from_reexports(*module, export_name.clone()).await?;
204            match &*result {
205                FindExportFromReexportsResult::NotFound => {
206                    return Ok(FollowExportsResult::cell(FollowExportsResult {
207                        module,
208                        export_name: Some(export_name),
209                        ty: FoundExportType::NotFound,
210                    }));
211                }
212                FindExportFromReexportsResult::EsmExport(esm_export) => {
213                    match handle_declared_export(module, export_name, esm_export).await? {
214                        ControlFlow::Continue((m, n)) => {
215                            module = m.to_resolved().await?;
216                            export_name = n;
217                            continue;
218                        }
219                        ControlFlow::Break(result) => {
220                            return Ok(result.cell());
221                        }
222                    }
223                }
224                FindExportFromReexportsResult::Dynamic(dynamic_exporting_modules) => {
225                    return match &dynamic_exporting_modules[..] {
226                        [] => unreachable!(),
227                        [module] => Ok(FollowExportsResult {
228                            module: *module,
229                            export_name: Some(export_name),
230                            ty: FoundExportType::Dynamic,
231                        }
232                        .cell()),
233                        _ => Ok(FollowExportsResult {
234                            module,
235                            export_name: Some(export_name),
236                            ty: FoundExportType::Dynamic,
237                        }
238                        .cell()),
239                    };
240                }
241            }
242        }
243
244        return Ok(FollowExportsResult::cell(FollowExportsResult {
245            module,
246            export_name: Some(export_name),
247            ty: FoundExportType::NotFound,
248        }));
249    }
250}
251
252pub async fn apply_reexport_tree_shaking(
253    module: Vc<Box<dyn EcmascriptChunkPlaceable>>,
254    part: ModulePart,
255) -> Result<Vc<Box<dyn Module>>> {
256    if let ModulePart::Export(export) = &part {
257        let FollowExportsResult {
258            module: final_module,
259            export_name: new_export,
260            ..
261        } = &*follow_reexports(module, export.clone(), true).await?;
262        let module = if let Some(new_export) = new_export {
263            if *new_export == *export {
264                Vc::upcast(**final_module)
265            } else {
266                Vc::upcast(EcmascriptModuleRenameModule::new(
267                    **final_module,
268                    ModulePart::renamed_export(new_export.clone(), export.clone()),
269                ))
270            }
271        } else {
272            Vc::upcast(EcmascriptModuleRenameModule::new(
273                **final_module,
274                ModulePart::renamed_namespace(export.clone()),
275            ))
276        };
277        return Ok(module);
278    }
279    Ok(Vc::upcast(module))
280}
281
282async fn handle_declared_export(
283    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
284    export_name: RcStr,
285    export: &EsmExport,
286) -> Result<ControlFlow<FollowExportsResult, (Vc<Box<dyn EcmascriptChunkPlaceable>>, RcStr)>> {
287    match export {
288        EsmExport::ImportedBinding(reference, name, _) => {
289            if let ReferencedAsset::Some(module) =
290                ReferencedAsset::from_resolve_result(reference.resolve_reference()).await?
291            {
292                return Ok(ControlFlow::Continue((*module, name.clone())));
293            }
294        }
295        EsmExport::ImportedNamespace(reference) => {
296            if let ReferencedAsset::Some(module) =
297                ReferencedAsset::from_resolve_result(reference.resolve_reference()).await?
298            {
299                return Ok(ControlFlow::Break(FollowExportsResult {
300                    module,
301                    export_name: None,
302                    ty: FoundExportType::Found,
303                }));
304            }
305        }
306        EsmExport::LocalBinding(..) => {
307            return Ok(ControlFlow::Break(FollowExportsResult {
308                module,
309                export_name: Some(export_name),
310                ty: FoundExportType::Found,
311            }));
312        }
313        EsmExport::Error => {
314            return Ok(ControlFlow::Break(FollowExportsResult {
315                module,
316                export_name: Some(export_name),
317                ty: FoundExportType::Unknown,
318            }));
319        }
320    }
321    Ok(ControlFlow::Break(FollowExportsResult {
322        module,
323        export_name: Some(export_name),
324        ty: FoundExportType::Unknown,
325    }))
326}
327
328#[turbo_tasks::value]
329enum FindExportFromReexportsResult {
330    NotFound,
331    EsmExport(EsmExport),
332    Dynamic(Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>),
333}
334
335#[turbo_tasks::function]
336async fn find_export_from_reexports(
337    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
338    export_name: RcStr,
339) -> Result<Vc<FindExportFromReexportsResult>> {
340    // TODO why do we need a special case for this?
341    if let Some(module) = ResolvedVc::try_downcast_type::<EcmascriptModulePartAsset>(module)
342        && matches!(module.await?.part, ModulePart::Exports)
343    {
344        let module_part = EcmascriptModulePartAsset::select_part(
345            *module.await?.full_module,
346            ModulePart::export(export_name.clone()),
347        );
348
349        // If we apply this logic to EcmascriptModuleAsset, we will resolve everything in the
350        // target module.
351        if (ResolvedVc::try_downcast_type::<EcmascriptModuleAsset>(
352            module_part.to_resolved().await?,
353        ))
354        .is_none()
355        {
356            return Ok(find_export_from_reexports(module_part, export_name));
357        }
358    }
359
360    let all_export_names = get_all_export_names(*module).await?;
361    Ok(
362        if let Some(esm_export) = all_export_names.esm_exports.get(&export_name) {
363            FindExportFromReexportsResult::EsmExport(esm_export.clone())
364        } else if all_export_names.dynamic_exporting_modules.is_empty() {
365            FindExportFromReexportsResult::NotFound
366        } else {
367            FindExportFromReexportsResult::Dynamic(
368                all_export_names.dynamic_exporting_modules.clone(),
369            )
370        }
371        .cell(),
372    )
373}
374
375#[turbo_tasks::value]
376struct AllExportNamesResult {
377    /// A map from export name to how each export is defined.
378    #[bincode(with = "turbo_bincode::indexmap")]
379    esm_exports: FxIndexMap<RcStr, EsmExport>,
380    /// A list of all direct or indirectly referenced modules that are dynamically exporting
381    dynamic_exporting_modules: Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
382}
383
384#[turbo_tasks::function]
385async fn get_all_export_names(
386    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
387) -> Result<Vc<AllExportNamesResult>> {
388    let exports = module.get_exports().await?;
389    let EcmascriptExports::EsmExports(exports) = &*exports else {
390        return Ok(AllExportNamesResult {
391            esm_exports: FxIndexMap::default(),
392            dynamic_exporting_modules: vec![module],
393        }
394        .cell());
395    };
396
397    let exports = exports.await?;
398    let mut esm_exports = FxIndexMap::default();
399    let mut dynamic_exporting_modules = Vec::new();
400    esm_exports.extend(
401        exports
402            .exports
403            .iter()
404            .map(|(name, esm_export)| (name.clone(), esm_export.clone())),
405    );
406    let star_export_names = exports
407        .star_exports
408        .iter()
409        .map(|esm_ref| async {
410            Ok(
411                if let ReferencedAsset::Some(m) =
412                    ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?
413                {
414                    Some(expand_star_exports(**esm_ref, *m))
415                } else {
416                    None
417                },
418            )
419        })
420        .try_flat_join()
421        .await?;
422    for star_export_names in star_export_names {
423        let star_export_names = star_export_names.await?;
424        esm_exports.extend(
425            star_export_names
426                .esm_exports
427                .iter()
428                .map(|(k, v)| (k.clone(), v.clone())),
429        );
430        dynamic_exporting_modules
431            .extend(star_export_names.dynamic_exporting_modules.iter().copied());
432    }
433
434    Ok(AllExportNamesResult {
435        esm_exports,
436        dynamic_exporting_modules,
437    }
438    .cell())
439}
440
441#[turbo_tasks::value]
442pub struct ExpandStarResult {
443    #[bincode(with = "turbo_bincode::indexmap")]
444    pub esm_exports: FxIndexMap<RcStr, EsmExport>,
445    pub dynamic_exporting_modules: Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
446}
447
448#[turbo_tasks::function]
449pub async fn expand_star_exports(
450    root_reference: ResolvedVc<Box<dyn ModuleReference>>,
451    root_module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
452) -> Result<Vc<ExpandStarResult>> {
453    let mut esm_exports = FxIndexMap::default();
454    let mut dynamic_exporting_modules = Vec::new();
455    let mut checked_modules = FxHashSet::default();
456    checked_modules.insert(root_module);
457    let mut queue = vec![(root_reference, root_module, root_module.get_exports())];
458    while let Some((reference, asset, exports)) = queue.pop() {
459        match &*exports.await? {
460            EcmascriptExports::EsmExports(exports) => {
461                let exports = exports.await?;
462                for (key, esm_export) in exports.exports.iter() {
463                    if key == "default" {
464                        continue;
465                    }
466                    if let Entry::Vacant(entry) = esm_exports.entry(key.clone()) {
467                        entry.insert(match esm_export {
468                            EsmExport::LocalBinding(_, liveness) => EsmExport::ImportedBinding(
469                                reference,
470                                key.clone(),
471                                *liveness == Liveness::Mutable,
472                            ),
473                            _ => esm_export.clone(),
474                        });
475                    }
476                }
477                for esm_ref in exports.star_exports.iter() {
478                    if let ReferencedAsset::Some(asset) =
479                        &ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?
480                        && checked_modules.insert(*asset)
481                    {
482                        queue.push((*esm_ref, *asset, asset.get_exports()));
483                    }
484                }
485            }
486            EcmascriptExports::None | EcmascriptExports::EmptyCommonJs => {
487                emit_star_exports_issue(
488                    asset.ident(),
489                    turbofmt!(
490                        "export * used with module {} which has no exports\nTypescript only: Did \
491                         you want to export only types with `export type * from \"...\"`?\nNote: \
492                         Using `export type` is more efficient than `export *` as it won't emit \
493                         any runtime code.",
494                        asset.ident()
495                    )
496                    .await?,
497                )
498                .await?
499            }
500            EcmascriptExports::Value => {
501                emit_star_exports_issue(
502                    asset.ident(),
503                    turbofmt!(
504                        "export * used with module {} which only has a default export (default \
505                         export is not exported with export *)\nDid you want to use `export {{ \
506                         default }} from \"...\";` instead?",
507                        asset.ident()
508                    )
509                    .await?,
510                )
511                .await?
512            }
513            EcmascriptExports::CommonJs => {
514                dynamic_exporting_modules.push(asset);
515                emit_star_exports_issue(
516                    asset.ident(),
517                    turbofmt!(
518                        "export * used with module {} which is a CommonJS module with exports \
519                         only available at runtime\nList all export names manually (`export {{ a, \
520                         b, c }} from \"...\") or rewrite the module to ESM, to avoid the \
521                         additional runtime code.`",
522                        asset.ident()
523                    )
524                    .await?,
525                )
526                .await?;
527            }
528            EcmascriptExports::DynamicNamespace => {
529                dynamic_exporting_modules.push(asset);
530            }
531            EcmascriptExports::Unknown => {
532                // Propagate the Unknown export type to a certain extent.
533                dynamic_exporting_modules.push(asset);
534            }
535        }
536    }
537
538    Ok(ExpandStarResult {
539        esm_exports,
540        dynamic_exporting_modules,
541    }
542    .cell())
543}
544
545async fn emit_star_exports_issue(source_ident: Vc<AssetIdent>, message: RcStr) -> Result<()> {
546    AnalyzeIssue::new(
547        IssueSeverity::Warning,
548        source_ident,
549        Vc::cell(rcstr!("unexpected export *")),
550        StyledString::Text(message).cell(),
551        None,
552        None,
553    )
554    .to_resolved()
555    .await?
556    .emit();
557    Ok(())
558}
559
560#[turbo_tasks::value(shared)]
561#[derive(Hash, Debug)]
562pub struct EsmExports {
563    /// Explicit exports
564    pub exports: FrozenMap<RcStr, EsmExport>,
565    /// Unexpanded `export * from ...` statements (expanded in `expand_star_exports`)
566    pub star_exports: Vec<ResolvedVc<Box<dyn ModuleReference>>>,
567}
568
569/// The expanded version of [`EsmExports`], the `exports` field here includes all exports that could
570/// be expanded from `star_exports`.
571///
572/// [`EsmExports::star_exports`] that could not be (fully) expanded end up in `dynamic_exports`.
573#[turbo_tasks::value(shared)]
574#[derive(Hash, Debug)]
575pub struct ExpandedExports {
576    pub exports: FrozenMap<RcStr, EsmExport>,
577    /// Modules we couldn't analyze all exports of.
578    pub dynamic_exports: Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
579}
580
581#[turbo_tasks::value_impl]
582impl EsmExports {
583    /// Creates an EsmExports that re-exports all exports from another module.
584    /// This is useful for wrapper modules that simply forward all exports.
585    ///
586    /// The resulting exports will have:
587    /// - A default export binding to the module's default
588    /// - A star export that re-exports all named exports
589    #[turbo_tasks::function]
590    pub async fn reexport_including_default(
591        module_reference: Vc<Box<dyn ModuleReference>>,
592    ) -> Result<Vc<EcmascriptExports>> {
593        let module_reference = module_reference.to_resolved().await?;
594        let mut exports = Vec::new();
595        let default = rcstr!("default");
596        exports.push((
597            default.clone(),
598            EsmExport::ImportedBinding(module_reference, default, false),
599        ));
600
601        Ok(EcmascriptExports::EsmExports(
602            EsmExports {
603                exports: FrozenMap::from(exports),
604                star_exports: vec![module_reference],
605            }
606            .resolved_cell(),
607        )
608        .cell())
609    }
610
611    #[turbo_tasks::function]
612    pub async fn expand_exports(
613        &self,
614        export_usage_info: Vc<ModuleExportUsageInfo>,
615    ) -> Result<Vc<ExpandedExports>> {
616        let mut exports: BTreeMap<_, _> = self
617            .exports
618            .iter()
619            .map(|(k, v)| (k.clone(), v.clone()))
620            .collect();
621        let mut dynamic_exports = vec![];
622        let export_usage_info = export_usage_info.await?;
623
624        if !matches!(*export_usage_info, ModuleExportUsageInfo::All) {
625            exports.retain(|export, _| export_usage_info.is_export_used(export));
626        }
627
628        for &esm_ref in self.star_exports.iter() {
629            // TODO(PACK-2176): we probably need to handle re-exporting from external
630            // modules.
631            let ReferencedAsset::Some(asset) =
632                &ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?
633            else {
634                continue;
635            };
636
637            let export_info = expand_star_exports(*esm_ref, **asset).await?;
638
639            for export in export_info.esm_exports.keys() {
640                if export == "default" {
641                    continue;
642                }
643                if !export_usage_info.is_export_used(export) {
644                    continue;
645                }
646
647                // the spec indicates first-one-wins: https://tc39.es/ecma262/#_ref_9060
648                exports
649                    .entry(export.clone())
650                    .or_insert_with(|| EsmExport::ImportedBinding(esm_ref, export.clone(), false));
651            }
652
653            if !export_info.dynamic_exporting_modules.is_empty() {
654                dynamic_exports.push(*asset);
655            }
656        }
657
658        Ok(ExpandedExports {
659            exports: FrozenMap::from(exports),
660            dynamic_exports,
661        }
662        .cell())
663    }
664}
665
666impl EsmExports {
667    pub async fn code_generation(
668        self: Vc<Self>,
669        chunking_context: Vc<Box<dyn ChunkingContext>>,
670        scope_hoisting_context: ScopeHoistingContext<'_>,
671        eval_context: &EvalContext,
672        module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
673    ) -> Result<CodeGeneration> {
674        let export_usage_info = chunking_context
675            .module_export_usage(*ResolvedVc::upcast(module))
676            .await?;
677        let expanded = self.expand_exports(*export_usage_info.export_usage).await?;
678
679        if scope_hoisting_context.skip_module_exports() && expanded.dynamic_exports.is_empty() {
680            // If the current module is not exposed, no need to generate exports.
681            //
682            // If there are dynamic_exports, we still need to export everything because it wasn't
683            // possible to determine statically where a reexport is coming from which will instead
684            // be handled at runtime via property access, e.g. `export * from "./some-dynamic-cjs"`
685            return Ok(CodeGeneration::empty());
686        }
687
688        let mut dynamic_exports = Vec::<Box<Expr>>::new();
689        {
690            let id = if let Some(module) = scope_hoisting_context.module()
691                && !expanded.dynamic_exports.is_empty()
692            {
693                Some(module.chunk_item_id(chunking_context).await?)
694            } else {
695                None
696            };
697
698            for dynamic_export_asset in &expanded.dynamic_exports {
699                let ident = ReferencedAsset::get_ident_from_placeable(
700                    dynamic_export_asset,
701                    chunking_context,
702                )
703                .await?;
704
705                if let Some(id) = &id {
706                    dynamic_exports.push(quote_expr!(
707                        "$turbopack_dynamic($arg, $id)",
708                        turbopack_dynamic: Expr = TURBOPACK_DYNAMIC.into(),
709                        arg: Expr = Ident::new(ident.into(), DUMMY_SP, Default::default()).into(),
710                        id: Expr = module_id_to_lit(id)
711                    ));
712                } else {
713                    dynamic_exports.push(quote_expr!(
714                        "$turbopack_dynamic($arg)",
715                        turbopack_dynamic: Expr = TURBOPACK_DYNAMIC.into(),
716                        arg: Expr = Ident::new(ident.into(), DUMMY_SP, Default::default()).into()
717                    ));
718                }
719            }
720        }
721
722        #[derive(Eq, PartialEq)]
723        enum ExportBinding {
724            Getter(Expr),
725            GetterSetter(Expr, Expr),
726            Value(Expr),
727            None,
728        }
729
730        let mut getters = Vec::new();
731        for (exported, local) in &expanded.exports {
732            let exprs: ExportBinding = match local {
733                EsmExport::Error => ExportBinding::Getter(quote!(
734                    "(() => { throw new Error(\"Failed binding. See build errors!\"); })" as Expr,
735                )),
736                EsmExport::LocalBinding(name, liveness) => {
737                    // TODO ideally, this information would just be stored in
738                    // EsmExport::LocalBinding and we wouldn't have to re-correlated this
739                    // information with eval_context.imports.exports to get the syntax context.
740                    let binding = if let Some((local, ctxt)) =
741                        eval_context.imports.exports_ids.get(exported)
742                    {
743                        Some((local.clone(), *ctxt))
744                    } else {
745                        bail!(
746                            "Expected export to be in eval context {:?} {:?}",
747                            exported,
748                            eval_context.imports,
749                        )
750                    };
751                    let (local, ctxt) = binding.unwrap_or_else(|| {
752                        // Fallback, shouldn't happen in practice
753                        (
754                            if name == "default" {
755                                MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM.clone()
756                            } else {
757                                name.as_str().into()
758                            },
759                            SyntaxContext::empty(),
760                        )
761                    });
762
763                    let local = Ident::new(local, DUMMY_SP, ctxt);
764                    match (liveness, export_usage_info.is_circuit_breaker) {
765                        (Liveness::Constant, false) => ExportBinding::Value(Expr::Ident(local)),
766                        // If the value might change or we are a circuit breaker we must bind a
767                        // getter to avoid capturing the value at the wrong time.
768                        (Liveness::Live, _) | (Liveness::Constant, true) => {
769                            ExportBinding::Getter(quote!("() => $local" as Expr, local = local))
770                        }
771                        (Liveness::Mutable, _) => ExportBinding::GetterSetter(
772                            quote!("() => $local" as Expr, local = local.clone()),
773                            quote!(
774                                "($new) => $local = $new" as Expr,
775                                local: AssignTarget = AssignTarget::Simple(local.into()),
776                                new = Ident::new(format!("new_{name}").into(), DUMMY_SP, ctxt),
777                            ),
778                        ),
779                    }
780                }
781                EsmExport::ImportedBinding(esm_ref, name, mutable) => {
782                    let referenced_asset =
783                        ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?;
784                    referenced_asset
785                        .get_ident(chunking_context, Some(name.clone()), scope_hoisting_context)
786                        .await?
787                        .map(|ident| {
788                            let expr = ident.as_expr_individual(DUMMY_SP);
789                            let read_expr = expr.map_either(Expr::from, Expr::from).into_inner();
790                            use crate::references::esm::base::ReferencedAssetIdent;
791                            match &ident {
792                                ReferencedAssetIdent::LocalBinding {ctxt, liveness,.. } => {
793                                    debug_assert!(*mutable == (*liveness == Liveness::Mutable), "If the re-export is mutable, the merged local must be too");
794                                    // If we are re-exporting something but got merged with it we can treat it like a local export
795                                     match (liveness, export_usage_info.is_circuit_breaker) {
796                                        (Liveness::Constant, false) => {
797                                            ExportBinding::Value(read_expr)
798                                        }
799                                        // If the value might change or we are a circuit breaker we must bind a
800                                        // getter to avoid capturing the value at the wrong time.
801                                        (Liveness::Live, _) | (Liveness::Constant, true) => {
802                                            // In the constant case, we could still export as a value if we knew that the module
803                                            // came _before_ us, but we don't at this point.
804                                            ExportBinding::Getter(quote!("() => $local" as Expr, local: Expr = read_expr))
805                                        }
806                                        (Liveness::Mutable, _) => {
807                                            let assign_target = AssignTarget::Simple(
808                                                        ident.as_expr_individual(DUMMY_SP).map_either(|i| SimpleAssignTarget::Ident(i.into()), SimpleAssignTarget::Member).into_inner());
809                                            ExportBinding::GetterSetter(
810                                                quote!("() => $local" as Expr, local: Expr= read_expr.clone()),
811                                                quote!(
812                                                    "($new) => $lhs = $new" as Expr,
813                                                    lhs: AssignTarget = assign_target,
814                                                    new = Ident::new(format!("new_{name}").into(), DUMMY_SP, *ctxt),
815                                                )
816                                            )
817                                        }
818                                    }
819                                },
820                                ReferencedAssetIdent::Module { .. } => {
821                                    // Otherwise we need to bind as a getter to preserve the 'liveness' of the other modules bindings.
822                                    // TODO: If this becomes important it might be faster to use the runtime to copy PropertyDescriptors across modules
823                                    // since that would reduce allocations and optimize access. We could do this by passing the module-id up.
824                                    let getter = quote!("() => $expr" as Expr, expr: Expr = read_expr);
825                                    let assign_target = AssignTarget::Simple(
826                                                    ident.as_expr_individual(DUMMY_SP).map_either(|i| SimpleAssignTarget::Ident(i.into()), SimpleAssignTarget::Member).into_inner());
827                                    if *mutable {
828                                        ExportBinding::GetterSetter(
829                                            getter,
830                                            quote!(
831                                                "($new) => $lhs = $new" as Expr,
832                                                lhs: AssignTarget = assign_target,
833                                                new = Ident::new(
834                                                    format!("new_{name}").into(),
835                                                    DUMMY_SP,
836                                                    Default::default()
837                                                ),
838                                            ))
839                                    } else {
840                                        ExportBinding::Getter(getter)
841                                    }
842                                }
843                            }
844                        }).unwrap_or(ExportBinding::None)
845                }
846                EsmExport::ImportedNamespace(esm_ref) => {
847                    let referenced_asset =
848                        ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?;
849                    referenced_asset
850                        .get_ident(chunking_context, None, scope_hoisting_context)
851                        .await?
852                        .map(|ident| {
853                            let imported = ident.as_expr(DUMMY_SP, false);
854                            if export_usage_info.is_circuit_breaker {
855                                ExportBinding::Getter(quote!(
856                                    "(() => $imported)" as Expr,
857                                    imported: Expr = imported
858                                ))
859                            } else {
860                                ExportBinding::Value(imported)
861                            }
862                        })
863                        .unwrap_or(ExportBinding::None)
864                }
865            };
866            if exprs != ExportBinding::None {
867                getters.push(Some(
868                    Expr::Lit(Lit::Str(Str {
869                        span: DUMMY_SP,
870                        value: exported.as_str().into(),
871                        raw: None,
872                    }))
873                    .into(),
874                ));
875                match exprs {
876                    ExportBinding::Getter(getter) => {
877                        getters.push(Some(getter.into()));
878                    }
879                    ExportBinding::GetterSetter(getter, setter) => {
880                        getters.push(Some(getter.into()));
881                        getters.push(Some(setter.into()));
882                    }
883                    ExportBinding::Value(value) => {
884                        // We need to push a discriminator in this case to make the fact that we are
885                        // binding a value unambiguous to the runtime.
886                        getters.push(Some(Expr::Lit(Lit::Num(Number::from(0))).into()));
887                        getters.push(Some(value.into()));
888                    }
889                    ExportBinding::None => {}
890                };
891            }
892        }
893        let getters = Expr::Array(ArrayLit {
894            span: DUMMY_SP,
895            elems: getters,
896        });
897        let dynamic_stmt = if !dynamic_exports.is_empty() {
898            vec![CodeGenerationHoistedStmt::new(
899                rcstr!("__turbopack_dynamic__"),
900                Stmt::Expr(ExprStmt {
901                    span: DUMMY_SP,
902                    expr: Expr::from_exprs(dynamic_exports),
903                }),
904            )]
905        } else {
906            vec![]
907        };
908
909        // When a module has dynamic re-exports (`export *` from a module whose
910        // exports are only known at runtime), its namespace object must stay
911        // extensible so the dynamic export proxy can surface those keys. Signal
912        // that to the runtime so it skips sealing the namespace.
913        let has_dynamic_exports = !expanded.dynamic_exports.is_empty();
914        let esm_exports = vec![CodeGenerationHoistedStmt::new(
915            rcstr!("__turbopack_esm__"),
916            if let Some(module) = scope_hoisting_context.module() {
917                let id = module.chunk_item_id(chunking_context).await?;
918                if has_dynamic_exports {
919                    quote!("$turbopack_esm($getters, $id, true);" as Stmt,
920                        turbopack_esm: Expr = TURBOPACK_ESM.into(),
921                        getters: Expr = getters,
922                        id: Expr = module_id_to_lit(&id)
923                    )
924                } else {
925                    quote!("$turbopack_esm($getters, $id);" as Stmt,
926                        turbopack_esm: Expr = TURBOPACK_ESM.into(),
927                        getters: Expr = getters,
928                        id: Expr = module_id_to_lit(&id)
929                    )
930                }
931            } else if has_dynamic_exports {
932                quote!("$turbopack_esm($getters, undefined, true);" as Stmt,
933                    turbopack_esm: Expr = TURBOPACK_ESM.into(),
934                    getters: Expr = getters
935                )
936            } else {
937                quote!("$turbopack_esm($getters);" as Stmt,
938                    turbopack_esm: Expr = TURBOPACK_ESM.into(),
939                    getters: Expr = getters
940                )
941            },
942        )];
943        // If we are a circuit breaker module we need to expose exports first so they are available
944        // to a cyclic importer otherwise we put them at the bottom of the module factory.
945        Ok(if export_usage_info.is_circuit_breaker {
946            CodeGeneration::new(vec![], dynamic_stmt, esm_exports, vec![], vec![])
947        } else {
948            CodeGeneration::new(vec![], vec![], vec![], dynamic_stmt, esm_exports)
949        })
950    }
951}