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, mangle::mangled_export_names},
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    /// Whether the keys these exports are emitted under may be shortened. Carried with the exports
537    /// so a module deriving its exports from another (facade, locals, part, rename) inherits it.
538    /// `mangle::mangled_export_names` decides whether they actually are.
539    pub mangle_export_names: bool,
540}
541
542/// The expanded version of [`EsmExports`], the `exports` field here includes all exports that could
543/// be expanded from `star_exports`.
544///
545/// [`EsmExports::star_exports`] that could not be (fully) expanded end up in `dynamic_exports`.
546#[turbo_tasks::value(shared)]
547#[derive(Hash, Debug)]
548pub struct ExpandedExports {
549    pub exports: FrozenMap<RcStr, EsmExport>,
550    /// Modules we couldn't analyze all exports of.
551    pub dynamic_exports: Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
552}
553
554#[turbo_tasks::value_impl]
555impl EsmExports {
556    /// Creates an EsmExports that re-exports all exports from another module.
557    /// This is useful for wrapper modules that simply forward all exports.
558    ///
559    /// The resulting exports will have:
560    /// - A default export binding to the module's default
561    /// - A star export that re-exports all named exports
562    #[turbo_tasks::function]
563    pub async fn reexport_including_default(
564        module_reference: Vc<Box<dyn ModuleReference>>,
565    ) -> Result<Vc<EcmascriptExports>> {
566        let module_reference = module_reference.to_resolved().await?;
567        let mut exports = Vec::new();
568        let default = rcstr!("default");
569        exports.push((
570            default.clone(),
571            EsmExport::ImportedBinding(module_reference, default, false),
572        ));
573
574        Ok(EcmascriptExports::EsmExports(
575            EsmExports {
576                exports: FrozenMap::from(exports),
577                star_exports: vec![module_reference],
578                // These facades exist so that a host framework can find the wrapped module's
579                // exports by name, so their keys have to stay as written.
580                mangle_export_names: false,
581            }
582            .resolved_cell(),
583        )
584        .cell())
585    }
586
587    #[turbo_tasks::function]
588    pub async fn expand_exports(
589        &self,
590        export_usage_info: Vc<ModuleExportUsageInfo>,
591    ) -> Result<Vc<ExpandedExports>> {
592        let mut exports: BTreeMap<_, _> = self
593            .exports
594            .iter()
595            .map(|(k, v)| (k.clone(), v.clone()))
596            .collect();
597        let mut dynamic_exports = vec![];
598        let export_usage_info = export_usage_info.await?;
599
600        if !matches!(*export_usage_info, ModuleExportUsageInfo::All) {
601            exports.retain(|export, _| export_usage_info.is_export_used(export));
602        }
603
604        for &esm_ref in self.star_exports.iter() {
605            // TODO(PACK-2176): we probably need to handle re-exporting from external
606            // modules.
607            let ReferencedAsset::Some(asset) =
608                &ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?
609            else {
610                continue;
611            };
612
613            let export_info = expand_star_exports(*esm_ref, **asset).await?;
614
615            for export in export_info.esm_exports.keys() {
616                if export == "default" {
617                    continue;
618                }
619                if !export_usage_info.is_export_used(export) {
620                    continue;
621                }
622
623                // the spec indicates first-one-wins: https://tc39.es/ecma262/#_ref_9060
624                exports
625                    .entry(export.clone())
626                    .or_insert_with(|| EsmExport::ImportedBinding(esm_ref, export.clone(), false));
627            }
628
629            if !export_info.dynamic_exporting_modules.is_empty() {
630                dynamic_exports.push(*asset);
631            }
632        }
633
634        Ok(ExpandedExports {
635            exports: FrozenMap::from(exports),
636            dynamic_exports,
637        }
638        .cell())
639    }
640}
641
642impl EsmExports {
643    pub async fn code_generation(
644        self: Vc<Self>,
645        chunking_context: Vc<Box<dyn ChunkingContext>>,
646        scope_hoisting_context: ScopeHoistingContext<'_>,
647        eval_context: &EvalContext,
648        module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
649    ) -> Result<CodeGeneration> {
650        let export_usage_info = chunking_context
651            .module_export_usage(*ResolvedVc::upcast(module))
652            .await?;
653        let expanded = self.expand_exports(*export_usage_info.export_usage).await?;
654
655        if scope_hoisting_context.skip_module_exports() && expanded.dynamic_exports.is_empty() {
656            // If the current module is not exposed, no need to generate exports.
657            //
658            // If there are dynamic_exports, we still need to export everything because it wasn't
659            // possible to determine statically where a reexport is coming from which will instead
660            // be handled at runtime via property access, e.g. `export * from "./some-dynamic-cjs"`
661            return Ok(CodeGeneration::empty());
662        }
663
664        let mut dynamic_exports = Vec::<Box<Expr>>::new();
665        {
666            let id = if let Some(module) = scope_hoisting_context.module()
667                && !expanded.dynamic_exports.is_empty()
668            {
669                Some(module.chunk_item_id(chunking_context).await?)
670            } else {
671                None
672            };
673
674            for dynamic_export_asset in &expanded.dynamic_exports {
675                let ident = ReferencedAsset::get_ident_from_placeable(
676                    dynamic_export_asset,
677                    chunking_context,
678                )
679                .await?;
680
681                if let Some(id) = &id {
682                    dynamic_exports.push(quote_expr!(
683                        "$turbopack_dynamic($arg, $id)",
684                        turbopack_dynamic: Expr = TURBOPACK_DYNAMIC.into(),
685                        arg: Expr = Ident::new(ident.into(), DUMMY_SP, Default::default()).into(),
686                        id: Expr = module_id_to_lit(id)
687                    ));
688                } else {
689                    dynamic_exports.push(quote_expr!(
690                        "$turbopack_dynamic($arg)",
691                        turbopack_dynamic: Expr = TURBOPACK_DYNAMIC.into(),
692                        arg: Expr = Ident::new(ident.into(), DUMMY_SP, Default::default()).into()
693                    ));
694                }
695            }
696        }
697
698        #[derive(Eq, PartialEq)]
699        enum ExportBinding {
700            Getter(Expr),
701            GetterSetter(Expr, Expr),
702            Value(Expr),
703            None,
704        }
705
706        let mut getters = Vec::new();
707        // The keys this module's exports are emitted under. Consumers resolve the same map for this
708        // module (see `ReferencedAsset::get_ident_inner`), so both sides always agree.
709        let mangled_names = mangled_export_names(*module, chunking_context).await?;
710        for (exported, local) in &expanded.exports {
711            let exprs: ExportBinding = match local {
712                EsmExport::Error => ExportBinding::Getter(quote!(
713                    "(() => { throw new Error(\"Failed binding. See build errors!\"); })" as Expr,
714                )),
715                EsmExport::LocalBinding(name, liveness) => {
716                    // TODO ideally, this information would just be stored in
717                    // EsmExport::LocalBinding and we wouldn't have to re-correlated this
718                    // information with eval_context.imports.exports to get the syntax context.
719                    let binding = if let Some((local, ctxt)) = eval_context
720                        .imports
721                        .exports_ids
722                        .get(exported)
723                        .map(|(id, _)| id)
724                    {
725                        Some((local.clone(), *ctxt))
726                    } else {
727                        bail!(
728                            "Expected export to be in eval context {:?} {:?}",
729                            exported,
730                            eval_context.imports,
731                        )
732                    };
733                    let (local, ctxt) = binding.unwrap_or_else(|| {
734                        // Fallback, shouldn't happen in practice
735                        (
736                            if name == "default" {
737                                MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM.clone()
738                            } else {
739                                name.as_str().into()
740                            },
741                            SyntaxContext::empty(),
742                        )
743                    });
744
745                    let local = Ident::new(local, DUMMY_SP, ctxt);
746                    match (liveness, export_usage_info.is_circuit_breaker) {
747                        (Liveness::Constant, false) => ExportBinding::Value(Expr::Ident(local)),
748                        // If the value might change or we are a circuit breaker we must bind a
749                        // getter to avoid capturing the value at the wrong time.
750                        (Liveness::Live, _) | (Liveness::Constant, true) => {
751                            ExportBinding::Getter(quote!("() => $local" as Expr, local = local))
752                        }
753                        (Liveness::Mutable, _) => ExportBinding::GetterSetter(
754                            quote!("() => $local" as Expr, local = local.clone()),
755                            quote!(
756                                "($new) => $local = $new" as Expr,
757                                local: AssignTarget = AssignTarget::Simple(local.into()),
758                                new = Ident::new(format!("new_{name}").into(), DUMMY_SP, ctxt),
759                            ),
760                        ),
761                    }
762                }
763                EsmExport::ImportedBinding(esm_ref, name, mutable) => {
764                    let referenced_asset =
765                        ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?;
766                    referenced_asset
767                        .get_ident(chunking_context, Some(name.clone()), scope_hoisting_context)
768                        .await?
769                        .map(|ident| {
770                            let expr = ident.as_expr_individual(DUMMY_SP);
771                            let read_expr = expr.map_either(Expr::from, Expr::from).into_inner();
772                            use crate::references::esm::base::ReferencedAssetIdent;
773                            match &ident {
774                                ReferencedAssetIdent::LocalBinding {ctxt, liveness,.. } => {
775                                    debug_assert!(*mutable == (*liveness == Liveness::Mutable), "If the re-export is mutable, the merged local must be too");
776                                    // If we are re-exporting something but got merged with it we can treat it like a local export
777                                     match (liveness, export_usage_info.is_circuit_breaker) {
778                                        (Liveness::Constant, false) => {
779                                            ExportBinding::Value(read_expr)
780                                        }
781                                        // If the value might change or we are a circuit breaker we must bind a
782                                        // getter to avoid capturing the value at the wrong time.
783                                        (Liveness::Live, _) | (Liveness::Constant, true) => {
784                                            // In the constant case, we could still export as a value if we knew that the module
785                                            // came _before_ us, but we don't at this point.
786                                            ExportBinding::Getter(quote!("() => $local" as Expr, local: Expr = read_expr))
787                                        }
788                                        (Liveness::Mutable, _) => {
789                                            let assign_target = AssignTarget::Simple(
790                                                        ident.as_expr_individual(DUMMY_SP).map_either(|i| SimpleAssignTarget::Ident(i.into()), SimpleAssignTarget::Member).into_inner());
791                                            ExportBinding::GetterSetter(
792                                                quote!("() => $local" as Expr, local: Expr= read_expr.clone()),
793                                                quote!(
794                                                    "($new) => $lhs = $new" as Expr,
795                                                    lhs: AssignTarget = assign_target,
796                                                    new = Ident::new(format!("new_{name}").into(), DUMMY_SP, *ctxt),
797                                                )
798                                            )
799                                        }
800                                    }
801                                },
802                                ReferencedAssetIdent::Module { .. } => {
803                                    // Otherwise we need to bind as a getter to preserve the 'liveness' of the other modules bindings.
804                                    // TODO: If this becomes important it might be faster to use the runtime to copy PropertyDescriptors across modules
805                                    // since that would reduce allocations and optimize access. We could do this by passing the module-id up.
806                                    let getter = quote!("() => $expr" as Expr, expr: Expr = read_expr);
807                                    let assign_target = AssignTarget::Simple(
808                                                    ident.as_expr_individual(DUMMY_SP).map_either(|i| SimpleAssignTarget::Ident(i.into()), SimpleAssignTarget::Member).into_inner());
809                                    if *mutable {
810                                        ExportBinding::GetterSetter(
811                                            getter,
812                                            quote!(
813                                                "($new) => $lhs = $new" as Expr,
814                                                lhs: AssignTarget = assign_target,
815                                                new = Ident::new(
816                                                    format!("new_{name}").into(),
817                                                    DUMMY_SP,
818                                                    Default::default()
819                                                ),
820                                            ))
821                                    } else {
822                                        ExportBinding::Getter(getter)
823                                    }
824                                }
825                            }
826                        }).unwrap_or(ExportBinding::None)
827                }
828                EsmExport::ImportedNamespace(esm_ref) => {
829                    let referenced_asset =
830                        ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?;
831                    referenced_asset
832                        .get_ident(chunking_context, None, scope_hoisting_context)
833                        .await?
834                        .map(|ident| {
835                            let imported = ident.as_expr(DUMMY_SP, false);
836                            if export_usage_info.is_circuit_breaker {
837                                ExportBinding::Getter(quote!(
838                                    "(() => $imported)" as Expr,
839                                    imported: Expr = imported
840                                ))
841                            } else {
842                                ExportBinding::Value(imported)
843                            }
844                        })
845                        .unwrap_or(ExportBinding::None)
846                }
847            };
848            if exprs != ExportBinding::None {
849                getters.push(Some(
850                    Expr::Lit(Lit::Str(Str {
851                        span: DUMMY_SP,
852                        // The key this export is emitted under: the mangled one when this module's
853                        // names are shortened, otherwise the original.
854                        value: mangled_names
855                            .as_ref()
856                            .and_then(|names| names.get(exported))
857                            .unwrap_or(exported)
858                            .as_str()
859                            .into(),
860                        raw: None,
861                    }))
862                    .into(),
863                ));
864                match exprs {
865                    ExportBinding::Getter(getter) => {
866                        getters.push(Some(getter.into()));
867                    }
868                    ExportBinding::GetterSetter(getter, setter) => {
869                        getters.push(Some(getter.into()));
870                        getters.push(Some(setter.into()));
871                    }
872                    ExportBinding::Value(value) => {
873                        // We need to push a discriminator in this case to make the fact that we are
874                        // binding a value unambiguous to the runtime.
875                        getters.push(Some(Expr::Lit(Lit::Num(Number::from(0))).into()));
876                        getters.push(Some(value.into()));
877                    }
878                    ExportBinding::None => {}
879                };
880            }
881        }
882        let getters = Expr::Array(ArrayLit {
883            span: DUMMY_SP,
884            elems: getters,
885        });
886        let dynamic_stmt = if !dynamic_exports.is_empty() {
887            vec![CodeGenerationHoistedStmt::new(
888                rcstr!("__turbopack_dynamic__"),
889                Stmt::Expr(ExprStmt {
890                    span: DUMMY_SP,
891                    expr: Expr::from_exprs(dynamic_exports),
892                }),
893            )]
894        } else {
895            vec![]
896        };
897
898        // When a module has dynamic re-exports (`export *` from a module whose
899        // exports are only known at runtime), its namespace object must stay
900        // extensible so the dynamic export proxy can surface those keys. Signal
901        // that to the runtime so it skips sealing the namespace.
902        let has_dynamic_exports = !expanded.dynamic_exports.is_empty();
903        let esm_exports = vec![CodeGenerationHoistedStmt::new(
904            rcstr!("__turbopack_esm__"),
905            if let Some(module) = scope_hoisting_context.module() {
906                let id = module.chunk_item_id(chunking_context).await?;
907                if has_dynamic_exports {
908                    quote!("$turbopack_esm($getters, $id, true);" as Stmt,
909                        turbopack_esm: Expr = TURBOPACK_ESM.into(),
910                        getters: Expr = getters,
911                        id: Expr = module_id_to_lit(&id)
912                    )
913                } else {
914                    quote!("$turbopack_esm($getters, $id);" as Stmt,
915                        turbopack_esm: Expr = TURBOPACK_ESM.into(),
916                        getters: Expr = getters,
917                        id: Expr = module_id_to_lit(&id)
918                    )
919                }
920            } else if has_dynamic_exports {
921                quote!("$turbopack_esm($getters, undefined, true);" as Stmt,
922                    turbopack_esm: Expr = TURBOPACK_ESM.into(),
923                    getters: Expr = getters
924                )
925            } else {
926                quote!("$turbopack_esm($getters);" as Stmt,
927                    turbopack_esm: Expr = TURBOPACK_ESM.into(),
928                    getters: Expr = getters
929                )
930            },
931        )];
932        // If we are a circuit breaker module we need to expose exports first so they are available
933        // to a cyclic importer otherwise we put them at the bottom of the module factory.
934        Ok(if export_usage_info.is_circuit_breaker {
935            CodeGeneration::new(vec![], dynamic_stmt, esm_exports, vec![], vec![])
936        } else {
937            CodeGeneration::new(vec![], vec![], vec![], dynamic_stmt, esm_exports)
938        })
939    }
940}