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