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    references::esm::base::ReferencedAsset,
36    runtime_functions::{TURBOPACK_DYNAMIC, TURBOPACK_ESM},
37    tree_shake::part::module::EcmascriptModulePartAsset,
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(|esm_ref| async {
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)) =
710                        eval_context.imports.exports_ids.get(exported)
711                    {
712                        Some((local.clone(), *ctxt))
713                    } else {
714                        bail!(
715                            "Expected export to be in eval context {:?} {:?}",
716                            exported,
717                            eval_context.imports,
718                        )
719                    };
720                    let (local, ctxt) = binding.unwrap_or_else(|| {
721                        // Fallback, shouldn't happen in practice
722                        (
723                            if name == "default" {
724                                MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM.clone()
725                            } else {
726                                name.as_str().into()
727                            },
728                            SyntaxContext::empty(),
729                        )
730                    });
731
732                    let local = Ident::new(local, DUMMY_SP, ctxt);
733                    match (liveness, export_usage_info.is_circuit_breaker) {
734                        (Liveness::Constant, false) => ExportBinding::Value(Expr::Ident(local)),
735                        // If the value might change or we are a circuit breaker we must bind a
736                        // getter to avoid capturing the value at the wrong time.
737                        (Liveness::Live, _) | (Liveness::Constant, true) => {
738                            ExportBinding::Getter(quote!("() => $local" as Expr, local = local))
739                        }
740                        (Liveness::Mutable, _) => ExportBinding::GetterSetter(
741                            quote!("() => $local" as Expr, local = local.clone()),
742                            quote!(
743                                "($new) => $local = $new" as Expr,
744                                local: AssignTarget = AssignTarget::Simple(local.into()),
745                                new = Ident::new(format!("new_{name}").into(), DUMMY_SP, ctxt),
746                            ),
747                        ),
748                    }
749                }
750                EsmExport::ImportedBinding(esm_ref, name, mutable) => {
751                    let referenced_asset =
752                        ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?;
753                    referenced_asset
754                        .get_ident(chunking_context, Some(name.clone()), scope_hoisting_context)
755                        .await?
756                        .map(|ident| {
757                            let expr = ident.as_expr_individual(DUMMY_SP);
758                            let read_expr = expr.map_either(Expr::from, Expr::from).into_inner();
759                            use crate::references::esm::base::ReferencedAssetIdent;
760                            match &ident {
761                                ReferencedAssetIdent::LocalBinding {ctxt, liveness,.. } => {
762                                    debug_assert!(*mutable == (*liveness == Liveness::Mutable), "If the re-export is mutable, the merged local must be too");
763                                    // If we are re-exporting something but got merged with it we can treat it like a local export
764                                     match (liveness, export_usage_info.is_circuit_breaker) {
765                                        (Liveness::Constant, false) => {
766                                            ExportBinding::Value(read_expr)
767                                        }
768                                        // If the value might change or we are a circuit breaker we must bind a
769                                        // getter to avoid capturing the value at the wrong time.
770                                        (Liveness::Live, _) | (Liveness::Constant, true) => {
771                                            // In the constant case, we could still export as a value if we knew that the module
772                                            // came _before_ us, but we don't at this point.
773                                            ExportBinding::Getter(quote!("() => $local" as Expr, local: Expr = read_expr))
774                                        }
775                                        (Liveness::Mutable, _) => {
776                                            let assign_target = AssignTarget::Simple(
777                                                        ident.as_expr_individual(DUMMY_SP).map_either(|i| SimpleAssignTarget::Ident(i.into()), SimpleAssignTarget::Member).into_inner());
778                                            ExportBinding::GetterSetter(
779                                                quote!("() => $local" as Expr, local: Expr= read_expr.clone()),
780                                                quote!(
781                                                    "($new) => $lhs = $new" as Expr,
782                                                    lhs: AssignTarget = assign_target,
783                                                    new = Ident::new(format!("new_{name}").into(), DUMMY_SP, *ctxt),
784                                                )
785                                            )
786                                        }
787                                    }
788                                },
789                                ReferencedAssetIdent::Module { namespace_ident:_, ctxt:_, export:_ } => {
790                                    // Otherwise we need to bind as a getter to preserve the 'liveness' of the other modules bindings.
791                                    // TODO: If this becomes important it might be faster to use the runtime to copy PropertyDescriptors across modules
792                                    // since that would reduce allocations and optimize access. We could do this by passing the module-id up.
793                                    let getter = quote!("() => $expr" as Expr, expr: Expr = read_expr);
794                                    let assign_target = AssignTarget::Simple(
795                                                    ident.as_expr_individual(DUMMY_SP).map_either(|i| SimpleAssignTarget::Ident(i.into()), SimpleAssignTarget::Member).into_inner());
796                                    if *mutable {
797                                        ExportBinding::GetterSetter(
798                                            getter,
799                                            quote!(
800                                                "($new) => $lhs = $new" as Expr,
801                                                lhs: AssignTarget = assign_target,
802                                                new = Ident::new(
803                                                    format!("new_{name}").into(),
804                                                    DUMMY_SP,
805                                                    Default::default()
806                                                ),
807                                            ))
808                                    } else {
809                                        ExportBinding::Getter(getter)
810                                    }
811                                }
812                            }
813                        }).unwrap_or(ExportBinding::None)
814                }
815                EsmExport::ImportedNamespace(esm_ref) => {
816                    let referenced_asset =
817                        ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?;
818                    referenced_asset
819                        .get_ident(chunking_context, None, scope_hoisting_context)
820                        .await?
821                        .map(|ident| {
822                            let imported = ident.as_expr(DUMMY_SP, false);
823                            if export_usage_info.is_circuit_breaker {
824                                ExportBinding::Getter(quote!(
825                                    "(() => $imported)" as Expr,
826                                    imported: Expr = imported
827                                ))
828                            } else {
829                                ExportBinding::Value(imported)
830                            }
831                        })
832                        .unwrap_or(ExportBinding::None)
833                }
834            };
835            if exprs != ExportBinding::None {
836                getters.push(Some(
837                    Expr::Lit(Lit::Str(Str {
838                        span: DUMMY_SP,
839                        value: exported.as_str().into(),
840                        raw: None,
841                    }))
842                    .into(),
843                ));
844                match exprs {
845                    ExportBinding::Getter(getter) => {
846                        getters.push(Some(getter.into()));
847                    }
848                    ExportBinding::GetterSetter(getter, setter) => {
849                        getters.push(Some(getter.into()));
850                        getters.push(Some(setter.into()));
851                    }
852                    ExportBinding::Value(value) => {
853                        // We need to push a discriminator in this case to make the fact that we are
854                        // binding a value unambiguous to the runtime.
855                        getters.push(Some(Expr::Lit(Lit::Num(Number::from(0))).into()));
856                        getters.push(Some(value.into()));
857                    }
858                    ExportBinding::None => {}
859                };
860            }
861        }
862        let getters = Expr::Array(ArrayLit {
863            span: DUMMY_SP,
864            elems: getters,
865        });
866        let dynamic_stmt = if !dynamic_exports.is_empty() {
867            vec![CodeGenerationHoistedStmt::new(
868                rcstr!("__turbopack_dynamic__"),
869                Stmt::Expr(ExprStmt {
870                    span: DUMMY_SP,
871                    expr: Expr::from_exprs(dynamic_exports),
872                }),
873            )]
874        } else {
875            vec![]
876        };
877
878        let esm_exports = vec![CodeGenerationHoistedStmt::new(
879            rcstr!("__turbopack_esm__"),
880            if let Some(module) = scope_hoisting_context.module() {
881                let id = module.chunk_item_id(chunking_context).await?;
882                quote!("$turbopack_esm($getters, $id);" as Stmt,
883                    turbopack_esm: Expr = TURBOPACK_ESM.into(),
884                    getters: Expr = getters,
885                    id: Expr = module_id_to_lit(&id)
886                )
887            } else {
888                quote!("$turbopack_esm($getters);" as Stmt,
889                    turbopack_esm: Expr = TURBOPACK_ESM.into(),
890                    getters: Expr = getters
891                )
892            },
893        )];
894        // If we are a circuit breaker module we need to expose exports first so they are available
895        // to a cyclic importer otherwise we put them at the bottom of the module factory.
896        Ok(if export_usage_info.is_circuit_breaker {
897            CodeGeneration::new(vec![], dynamic_stmt, esm_exports, vec![], vec![])
898        } else {
899            CodeGeneration::new(vec![], vec![], vec![], dynamic_stmt, esm_exports)
900        })
901    }
902}