Skip to main content

turbopack_ecmascript/references/esm/
export.rs

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