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, Span, SyntaxContext},
9    ecma::ast::{
10        ArrayLit, AssignTarget, Expr, ExprOrSpread, ExprStmt, Ident, Lit, Number,
11        SimpleAssignTarget, Stmt, Str,
12    },
13    quote, quote_expr,
14};
15use turbo_frozenmap::FrozenMap;
16use turbo_rcstr::{RcStr, rcstr};
17use turbo_tasks::{FxIndexMap, NonLocalValue, ResolvedVc, TryFlatJoinIterExt, Vc, turbofmt};
18use turbopack_core::{
19    chunk::{ChunkingContext, ModuleChunkItemIdExt},
20    ident::AssetIdent,
21    issue::{IssueExt, IssueSeverity, analyze::AnalyzeIssue},
22    module::{Module, ModuleSideEffects},
23    module_graph::binding_usage_info::ModuleExportUsageInfo,
24    reference::ModuleReference,
25    resolve::ModulePart,
26};
27
28use crate::{
29    EcmascriptModuleAsset, ScopeHoistingContext,
30    analyzer::{
31        graph::EvalContext,
32        imports::{Export, ExportRegistrationMode},
33    },
34    chunk::{EcmascriptChunkPlaceable, EcmascriptExports},
35    code_gen::{CodeGeneration, CodeGenerationHoistedStmt},
36    magic_identifier::MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM,
37    module_fragments::part::module::EcmascriptModulePartAsset,
38    references::esm::{
39        base::{EsmAssetReference, ImportSource, ReferencedAsset, ReferencedAssetIdent},
40        mangle::mangled_export_names,
41    },
42    runtime_functions::{TURBOPACK_DYNAMIC, TURBOPACK_ESM, TURBOPACK_ESM_REEXPORT},
43    side_effect_optimization::reference::EcmascriptModulePartReference,
44    utils::module_id_to_lit,
45};
46
47/// Models the 'liveness' of an esm export
48/// All ESM exports are technically live but many never change and we can optimize representation to
49/// support that, this enum tracks the actual behavior of the export binding.
50#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, NonLocalValue, Encode, Decode)]
51pub enum Liveness {
52    // The binding never changes after module evaluation
53    Constant,
54    // The binding may change after module evaluation
55    Live,
56    // The binding needs to be exposed as mutable to callers.  This isn't part of the spec but is
57    // part of our module-fragments optimization where we split modules into parts and preserve
58    // mutability of variables via mutable exports.
59    Mutable,
60}
61
62#[derive(Clone, Hash, Debug, PartialEq, Eq, NonLocalValue, Encode, Decode)]
63pub enum EsmExport {
64    /// A local binding that is exported (export { a } or export const a = 1)
65    ///
66    /// Fields: (local_name, liveness)
67    LocalBinding(RcStr, Liveness),
68    /// An imported binding that is exported (export { a as b } from "...")
69    ///
70    /// Fields: (module_reference, name, is_mutable)
71    ImportedBinding(ResolvedVc<Box<dyn ModuleReference>>, RcStr, bool),
72    /// An imported namespace that is exported (export * from "...")
73    ImportedNamespace(ResolvedVc<Box<dyn ModuleReference>>),
74    /// An error occurred while resolving the export
75    Error,
76}
77
78#[turbo_tasks::function]
79pub async fn is_export_missing(
80    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
81    export_name: RcStr,
82) -> Result<Vc<bool>> {
83    if export_name == "__turbopack_module_id__" {
84        return Ok(Vc::cell(false));
85    }
86
87    let exports = module.get_exports().await?;
88    let exports = match &*exports {
89        EcmascriptExports::None => return Ok(Vc::cell(true)),
90        EcmascriptExports::Unknown => return Ok(Vc::cell(false)),
91        EcmascriptExports::Value => return Ok(Vc::cell(false)),
92        EcmascriptExports::CommonJs(_) => return Ok(Vc::cell(false)),
93        EcmascriptExports::EmptyCommonJs => return Ok(Vc::cell(export_name != "default")),
94        EcmascriptExports::DynamicNamespace => return Ok(Vc::cell(false)),
95        EcmascriptExports::EsmExports(exports) => *exports,
96    };
97
98    let exports = exports.await?;
99    if exports.exports.contains_key(&export_name) {
100        return Ok(Vc::cell(false));
101    }
102    if export_name == "default" {
103        return Ok(Vc::cell(true));
104    }
105
106    if exports.star_exports.is_empty() {
107        return Ok(Vc::cell(true));
108    }
109
110    let all_export_names = get_all_export_names(*module).await?;
111    if all_export_names.esm_exports.contains_key(&export_name) {
112        return Ok(Vc::cell(false));
113    }
114
115    for &dynamic_module in &all_export_names.dynamic_exporting_modules {
116        let exports = dynamic_module.get_exports().await?;
117        match &*exports {
118            EcmascriptExports::Value
119            | EcmascriptExports::CommonJs(_)
120            | EcmascriptExports::DynamicNamespace
121            | EcmascriptExports::Unknown => {
122                return Ok(Vc::cell(false));
123            }
124            EcmascriptExports::None
125            | EcmascriptExports::EmptyCommonJs
126            | EcmascriptExports::EsmExports(_) => {}
127        }
128    }
129
130    Ok(Vc::cell(true))
131}
132
133#[turbo_tasks::function]
134pub async fn all_known_export_names(
135    module: Vc<Box<dyn EcmascriptChunkPlaceable>>,
136) -> Result<Vc<Vec<RcStr>>> {
137    let export_names = get_all_export_names(module).await?;
138    Ok(Vc::cell(export_names.esm_exports.keys().cloned().collect()))
139}
140
141#[derive(Copy, Clone, Debug, PartialEq, Eq, NonLocalValue, Encode, Decode)]
142pub enum FoundExportType {
143    Found,
144    Dynamic,
145    NotFound,
146    SideEffects,
147    Unknown,
148}
149
150#[turbo_tasks::value]
151pub struct FollowExportsResult {
152    pub module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
153    pub export_name: Option<RcStr>,
154    pub ty: FoundExportType,
155}
156
157#[turbo_tasks::function]
158pub async fn follow_reexports(
159    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
160    export_name: RcStr,
161    ignore_side_effect_of_entry: bool,
162) -> Result<Vc<FollowExportsResult>> {
163    let mut ignore_side_effects = ignore_side_effect_of_entry;
164
165    let mut module = module;
166    let mut export_name = export_name;
167    loop {
168        if !ignore_side_effects
169            && *module.side_effects().await? != ModuleSideEffects::SideEffectFree
170        {
171            // TODO It's unfortunate that we have to use the whole module here.
172            // This is often the Facade module, which includes all reexports.
173            // Often we could use Locals + the followed reexports instead.
174            return Ok(FollowExportsResult::cell(FollowExportsResult {
175                module,
176                export_name: Some(export_name),
177                ty: FoundExportType::SideEffects,
178            }));
179        }
180        ignore_side_effects = false;
181
182        let exports = module.get_exports().await?;
183        let EcmascriptExports::EsmExports(exports) = &*exports else {
184            return Ok(FollowExportsResult::cell(FollowExportsResult {
185                module,
186                export_name: Some(export_name),
187                ty: FoundExportType::Dynamic,
188            }));
189        };
190
191        // Try to find the export in the local exports
192        let exports_ref = exports.await?;
193        if let Some(export) = exports_ref.exports.get(&export_name) {
194            match handle_declared_export(module, export_name, export).await? {
195                ControlFlow::Continue((m, n)) => {
196                    module = m.to_resolved().await?;
197                    export_name = n;
198                    continue;
199                }
200                ControlFlow::Break(result) => {
201                    return Ok(result.cell());
202                }
203            }
204        }
205
206        // Try to find the export in the star exports
207        if !exports_ref.star_exports.is_empty() && &*export_name != "default" {
208            let result = find_export_from_reexports(*module, export_name.clone()).await?;
209            match &*result {
210                FindExportFromReexportsResult::NotFound => {
211                    return Ok(FollowExportsResult::cell(FollowExportsResult {
212                        module,
213                        export_name: Some(export_name),
214                        ty: FoundExportType::NotFound,
215                    }));
216                }
217                FindExportFromReexportsResult::EsmExport(esm_export) => {
218                    match handle_declared_export(module, export_name, esm_export).await? {
219                        ControlFlow::Continue((m, n)) => {
220                            module = m.to_resolved().await?;
221                            export_name = n;
222                            continue;
223                        }
224                        ControlFlow::Break(result) => {
225                            return Ok(result.cell());
226                        }
227                    }
228                }
229                FindExportFromReexportsResult::Dynamic(dynamic_exporting_modules) => {
230                    return match &dynamic_exporting_modules[..] {
231                        [] => unreachable!(),
232                        [module] => Ok(FollowExportsResult {
233                            module: *module,
234                            export_name: Some(export_name),
235                            ty: FoundExportType::Dynamic,
236                        }
237                        .cell()),
238                        _ => Ok(FollowExportsResult {
239                            module,
240                            export_name: Some(export_name),
241                            ty: FoundExportType::Dynamic,
242                        }
243                        .cell()),
244                    };
245                }
246            }
247        }
248
249        return Ok(FollowExportsResult::cell(FollowExportsResult {
250            module,
251            export_name: Some(export_name),
252            ty: FoundExportType::NotFound,
253        }));
254    }
255}
256
257async fn handle_declared_export(
258    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
259    export_name: RcStr,
260    export: &EsmExport,
261) -> Result<ControlFlow<FollowExportsResult, (Vc<Box<dyn EcmascriptChunkPlaceable>>, RcStr)>> {
262    match export {
263        EsmExport::ImportedBinding(reference, name, _) => {
264            if let ReferencedAsset::Some(module) =
265                ReferencedAsset::from_resolve_result(reference.resolve_reference()).await?
266            {
267                return Ok(ControlFlow::Continue((*module, name.clone())));
268            }
269        }
270        EsmExport::ImportedNamespace(reference) => {
271            if let ReferencedAsset::Some(module) =
272                ReferencedAsset::from_resolve_result(reference.resolve_reference()).await?
273            {
274                return Ok(ControlFlow::Break(FollowExportsResult {
275                    module,
276                    export_name: None,
277                    ty: FoundExportType::Found,
278                }));
279            }
280        }
281        EsmExport::LocalBinding(..) => {
282            return Ok(ControlFlow::Break(FollowExportsResult {
283                module,
284                export_name: Some(export_name),
285                ty: FoundExportType::Found,
286            }));
287        }
288        EsmExport::Error => {
289            return Ok(ControlFlow::Break(FollowExportsResult {
290                module,
291                export_name: Some(export_name),
292                ty: FoundExportType::Unknown,
293            }));
294        }
295    }
296    Ok(ControlFlow::Break(FollowExportsResult {
297        module,
298        export_name: Some(export_name),
299        ty: FoundExportType::Unknown,
300    }))
301}
302
303#[turbo_tasks::value]
304enum FindExportFromReexportsResult {
305    NotFound,
306    EsmExport(EsmExport),
307    Dynamic(Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>),
308}
309
310#[turbo_tasks::function]
311async fn find_export_from_reexports(
312    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
313    export_name: RcStr,
314) -> Result<Vc<FindExportFromReexportsResult>> {
315    // TODO why do we need a special case for this?
316    if let Some(module) = ResolvedVc::try_downcast_type::<EcmascriptModulePartAsset>(module)
317        && matches!(module.await?.part, ModulePart::Exports)
318    {
319        let module_part = EcmascriptModulePartAsset::select_part(
320            *module.await?.full_module,
321            ModulePart::export(export_name.clone()),
322        );
323
324        // If we apply this logic to EcmascriptModuleAsset, we will resolve everything in the
325        // target module.
326        if (ResolvedVc::try_downcast_type::<EcmascriptModuleAsset>(
327            module_part.to_resolved().await?,
328        ))
329        .is_none()
330        {
331            return Ok(find_export_from_reexports(module_part, export_name));
332        }
333    }
334
335    let all_export_names = get_all_export_names(*module).await?;
336    Ok(
337        if let Some(esm_export) = all_export_names.esm_exports.get(&export_name) {
338            FindExportFromReexportsResult::EsmExport(esm_export.clone())
339        } else if all_export_names.dynamic_exporting_modules.is_empty() {
340            FindExportFromReexportsResult::NotFound
341        } else {
342            FindExportFromReexportsResult::Dynamic(
343                all_export_names.dynamic_exporting_modules.clone(),
344            )
345        }
346        .cell(),
347    )
348}
349
350#[turbo_tasks::value]
351struct AllExportNamesResult {
352    /// A map from export name to how each export is defined.
353    #[bincode(with = "turbo_bincode::indexmap")]
354    esm_exports: FxIndexMap<RcStr, EsmExport>,
355    /// A list of all direct or indirectly referenced modules that are dynamically exporting
356    dynamic_exporting_modules: Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
357}
358
359#[turbo_tasks::function]
360async fn get_all_export_names(
361    module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
362) -> Result<Vc<AllExportNamesResult>> {
363    let exports = module.get_exports().await?;
364    let EcmascriptExports::EsmExports(exports) = &*exports else {
365        return Ok(AllExportNamesResult {
366            esm_exports: FxIndexMap::default(),
367            dynamic_exporting_modules: vec![module],
368        }
369        .cell());
370    };
371
372    let exports = exports.await?;
373    let mut esm_exports = FxIndexMap::default();
374    let mut dynamic_exporting_modules = Vec::new();
375    esm_exports.extend(
376        exports
377            .exports
378            .iter()
379            .map(|(name, esm_export)| (name.clone(), esm_export.clone())),
380    );
381    let star_export_names = exports
382        .star_exports
383        .iter()
384        .map(async |esm_ref| {
385            Ok(
386                if let ReferencedAsset::Some(m) =
387                    ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?
388                {
389                    Some(expand_star_exports(**esm_ref, *m))
390                } else {
391                    None
392                },
393            )
394        })
395        .try_flat_join()
396        .await?;
397    for star_export_names in star_export_names {
398        let star_export_names = star_export_names.await?;
399        esm_exports.extend(
400            star_export_names
401                .esm_exports
402                .iter()
403                .map(|(k, v)| (k.clone(), v.clone())),
404        );
405        dynamic_exporting_modules
406            .extend(star_export_names.dynamic_exporting_modules.iter().copied());
407    }
408
409    Ok(AllExportNamesResult {
410        esm_exports,
411        dynamic_exporting_modules,
412    }
413    .cell())
414}
415
416#[turbo_tasks::value]
417pub struct ExpandStarResult {
418    #[bincode(with = "turbo_bincode::indexmap")]
419    pub esm_exports: FxIndexMap<RcStr, EsmExport>,
420    pub dynamic_exporting_modules: Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
421}
422
423#[turbo_tasks::function]
424pub async fn expand_star_exports(
425    root_reference: ResolvedVc<Box<dyn ModuleReference>>,
426    root_module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
427) -> Result<Vc<ExpandStarResult>> {
428    let mut esm_exports = FxIndexMap::default();
429    let mut dynamic_exporting_modules = Vec::new();
430    let mut checked_modules = FxHashSet::default();
431    checked_modules.insert(root_module);
432    let mut queue = vec![(root_reference, root_module, root_module.get_exports())];
433    while let Some((reference, asset, exports)) = queue.pop() {
434        match &*exports.await? {
435            EcmascriptExports::EsmExports(exports) => {
436                let exports = exports.await?;
437                for (key, esm_export) in exports.exports.iter() {
438                    if key == "default" {
439                        continue;
440                    }
441                    if let Entry::Vacant(entry) = esm_exports.entry(key.clone()) {
442                        entry.insert(match esm_export {
443                            EsmExport::LocalBinding(_, liveness) => EsmExport::ImportedBinding(
444                                reference,
445                                key.clone(),
446                                *liveness == Liveness::Mutable,
447                            ),
448                            _ => esm_export.clone(),
449                        });
450                    }
451                }
452                for esm_ref in exports.star_exports.iter() {
453                    if let ReferencedAsset::Some(asset) =
454                        &ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?
455                        && checked_modules.insert(*asset)
456                    {
457                        queue.push((*esm_ref, *asset, asset.get_exports()));
458                    }
459                }
460            }
461            EcmascriptExports::None | EcmascriptExports::EmptyCommonJs => {
462                emit_star_exports_issue(
463                    asset.ident(),
464                    turbofmt!(
465                        "export * used with module {} which has no exports\nTypescript only: Did \
466                         you want to export only types with `export type * from \"...\"`?\nNote: \
467                         Using `export type` is more efficient than `export *` as it won't emit \
468                         any runtime code.",
469                        asset.ident()
470                    )
471                    .await?,
472                )
473                .await?
474            }
475            EcmascriptExports::Value => {
476                emit_star_exports_issue(
477                    asset.ident(),
478                    turbofmt!(
479                        "export * used with module {} which only has a default export (default \
480                         export is not exported with export *)\nDid you want to use `export {{ \
481                         default }} from \"...\";` instead?",
482                        asset.ident()
483                    )
484                    .await?,
485                )
486                .await?
487            }
488            EcmascriptExports::CommonJs(_) => {
489                dynamic_exporting_modules.push(asset);
490                emit_star_exports_issue(
491                    asset.ident(),
492                    turbofmt!(
493                        "export * used with module {} which is a CommonJS module with exports \
494                         only available at runtime\nList all export names manually (`export {{ a, \
495                         b, c }} from \"...\") or rewrite the module to ESM, to avoid the \
496                         additional runtime code.`",
497                        asset.ident()
498                    )
499                    .await?,
500                )
501                .await?;
502            }
503            EcmascriptExports::DynamicNamespace => {
504                dynamic_exporting_modules.push(asset);
505            }
506            EcmascriptExports::Unknown => {
507                // Propagate the Unknown export type to a certain extent.
508                dynamic_exporting_modules.push(asset);
509            }
510        }
511    }
512
513    Ok(ExpandStarResult {
514        esm_exports,
515        dynamic_exporting_modules,
516    }
517    .cell())
518}
519
520async fn emit_star_exports_issue(source_ident: Vc<AssetIdent>, message: RcStr) -> Result<()> {
521    AnalyzeIssue::new(
522        IssueSeverity::Warning,
523        source_ident,
524        rcstr!("unexpected export *"),
525        message,
526        None,
527        None,
528    )
529    .to_resolved()
530    .await?
531    .emit();
532    Ok(())
533}
534
535#[turbo_tasks::value(shared)]
536#[derive(Hash, Debug)]
537pub struct EsmExports {
538    /// Explicit exports
539    pub exports: FrozenMap<RcStr, EsmExport>,
540    /// Unexpanded `export * from ...` statements (expanded in `expand_star_exports`)
541    pub star_exports: Vec<ResolvedVc<Box<dyn ModuleReference>>>,
542    /// Whether the keys these exports are emitted under may be shortened. Carried with the exports
543    /// so a module deriving its exports from another (facade, locals, part, rename) inherits it.
544    /// `mangle::mangled_export_names` decides whether they actually are.
545    pub mangle_export_names: bool,
546}
547
548/// The expanded version of [`EsmExports`], the `exports` field here includes all exports that could
549/// be expanded from `star_exports`.
550///
551/// [`EsmExports::star_exports`] that could not be (fully) expanded end up in `dynamic_exports`.
552#[turbo_tasks::value(shared)]
553#[derive(Hash, Debug)]
554pub struct ExpandedExports {
555    pub exports: FrozenMap<RcStr, EsmExport>,
556    /// Modules we couldn't analyze all exports of.
557    pub dynamic_exports: Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
558}
559
560#[turbo_tasks::value_impl]
561impl EsmExports {
562    /// Creates an EsmExports that re-exports all exports from another module.
563    /// This is useful for wrapper modules that simply forward all exports.
564    ///
565    /// The resulting exports will have:
566    /// - A default export binding to the module's default
567    /// - A star export that re-exports all named exports
568    #[turbo_tasks::function]
569    pub async fn reexport_including_default(
570        module_reference: Vc<Box<dyn ModuleReference>>,
571    ) -> Result<Vc<EcmascriptExports>> {
572        let module_reference = module_reference.to_resolved().await?;
573        let mut exports = Vec::new();
574        let default = rcstr!("default");
575        exports.push((
576            default.clone(),
577            EsmExport::ImportedBinding(module_reference, default, false),
578        ));
579
580        Ok(EcmascriptExports::EsmExports(
581            EsmExports {
582                exports: FrozenMap::from(exports),
583                star_exports: vec![module_reference],
584                // These facades exist so that a host framework can find the wrapped module's
585                // exports by name, so their keys have to stay as written.
586                mangle_export_names: false,
587            }
588            .resolved_cell(),
589        )
590        .cell())
591    }
592
593    #[turbo_tasks::function]
594    pub async fn expand_exports(
595        &self,
596        export_usage_info: Vc<ModuleExportUsageInfo>,
597    ) -> Result<Vc<ExpandedExports>> {
598        let mut exports: BTreeMap<_, _> = self
599            .exports
600            .iter()
601            .map(|(k, v)| (k.clone(), v.clone()))
602            .collect();
603        let mut dynamic_exports = vec![];
604        let export_usage_info = export_usage_info.await?;
605
606        if !matches!(*export_usage_info, ModuleExportUsageInfo::All) {
607            exports.retain(|export, _| export_usage_info.is_export_used(export));
608        }
609
610        for &esm_ref in self.star_exports.iter() {
611            // TODO(PACK-2176): we probably need to handle re-exporting from external
612            // modules.
613            let ReferencedAsset::Some(asset) =
614                &ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?
615            else {
616                continue;
617            };
618
619            let export_info = expand_star_exports(*esm_ref, **asset).await?;
620
621            for export in export_info.esm_exports.keys() {
622                if export == "default" {
623                    continue;
624                }
625                if !export_usage_info.is_export_used(export) {
626                    continue;
627                }
628
629                // the spec indicates first-one-wins: https://tc39.es/ecma262/#_ref_9060
630                exports
631                    .entry(export.clone())
632                    .or_insert_with(|| EsmExport::ImportedBinding(esm_ref, export.clone(), false));
633            }
634
635            if !export_info.dynamic_exporting_modules.is_empty() {
636                dynamic_exports.push(*asset);
637            }
638        }
639
640        Ok(ExpandedExports {
641            exports: FrozenMap::from(exports),
642            dynamic_exports,
643        }
644        .cell())
645    }
646}
647
648/// Resolves a synthetic facade reference to the namespace key and module used by a compact group.
649async fn compact_reference_target(
650    reference: ResolvedVc<Box<dyn ModuleReference>>,
651    chunking_context: Vc<Box<dyn ChunkingContext>>,
652    scope_hoisting_context: ScopeHoistingContext<'_>,
653) -> Result<Option<(NamespaceKey, ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>)>> {
654    let referenced_asset =
655        ReferencedAsset::from_resolve_result(reference.resolve_reference()).await?;
656    let Some(ReferencedAssetIdent::Module {
657        namespace_ident,
658        ctxt,
659        import_source: ImportSource::Module { asset },
660        ..
661    }) = referenced_asset
662        .get_ident(chunking_context, None, scope_hoisting_context)
663        .await?
664    else {
665        return Ok(None);
666    };
667    Ok(Some(((namespace_ident, ctxt), asset)))
668}
669
670/// Collects the exports into one group per source module, or returns `None` if any export does not
671/// fit the compact shape (a plain, immutable forward of a named binding out of an in-graph module)
672/// so the caller falls back to the general registration.
673async fn build_compact_reexports(
674    exports: &FrozenMap<RcStr, EsmExport>,
675    mangled_names: Option<&FrozenMap<RcStr, RcStr>>,
676    eval_context: &EvalContext,
677    mode: ExportRegistrationMode,
678    synthetic_references: Option<SyntheticReexportReferences<'_>>,
679    chunking_context: Vc<Box<dyn ChunkingContext>>,
680    scope_hoisting_context: ScopeHoistingContext<'_>,
681) -> Result<Option<CompactReexports>> {
682    let unused_references = chunking_context.unused_references().await?;
683    let locally_bound: FxHashSet<usize> = eval_context
684        .imports
685        .locally_bound_reference_idxs()
686        .collect();
687
688    // Keyed by the namespace variable, which names the resolved module: two exports forwarded from
689    // the same module share a group even when they came from different `export ... from` clauses.
690    let mut groups: FxIndexMap<NamespaceKey, ReexportGroup> = FxIndexMap::default();
691    let mut synthetic_order: FxIndexMap<NamespaceKey, usize> = FxIndexMap::default();
692    let mut positions_known = true;
693
694    if let Some((part_references, esm_references)) = synthetic_references {
695        // Structural part references run first. A retained facade locals reference may have no
696        // forwarded names at all, but the empty group still imports it for evaluation. Do not add
697        // a group for a reference the usage graph pruned: its target may not exist in this chunk,
698        // and its code generation deliberately emits no namespace binding either.
699        for (order, reference) in part_references.iter().enumerate() {
700            if unused_references.contains_key(&ResolvedVc::upcast(*reference)) {
701                continue;
702            }
703
704            let evaluation_only = reference.await?.is_evaluation_only();
705            if let Some((key, asset)) = compact_reference_target(
706                ResolvedVc::upcast(*reference),
707                chunking_context,
708                scope_hoisting_context,
709            )
710            .await?
711            {
712                // A merged evaluation reference only contributes the in-factory ordering
713                // placeholder; it intentionally creates no namespace variable for `a.S` to read.
714                if evaluation_only && scope_hoisting_context.get_module_index(asset).is_some() {
715                    continue;
716                }
717
718                synthetic_order.entry(key.clone()).or_insert(order);
719                groups.entry(key.clone()).or_insert_with(|| ReexportGroup {
720                    order,
721                    namespace_ident: key.0,
722                    ctxt: key.1,
723                    asset,
724                    locally_bound: false,
725                    reference_spans: FxHashSet::default(),
726                    pairs: Vec::new(),
727                });
728            }
729        }
730
731        let offset = part_references.len();
732        for (index, reference) in esm_references.iter().enumerate() {
733            // Unused re-exports are absent from the module ID map and cannot contribute
734            // an import or a source-order position to the compact registration.
735            if unused_references.contains_key(&ResolvedVc::upcast(*reference)) {
736                continue;
737            }
738
739            if let Some((key, _)) = compact_reference_target(
740                ResolvedVc::upcast(*reference),
741                chunking_context,
742                scope_hoisting_context,
743            )
744            .await?
745            {
746                synthetic_order.entry(key).or_insert(offset + index);
747            }
748        }
749    }
750
751    for (exported, local) in exports {
752        let EsmExport::ImportedBinding(esm_ref, imported_name, mutable) = local else {
753            return Ok(None);
754        };
755        // The usage graph can prune a re-export's target even though its name is still
756        // present in the facade's expanded export list. Do not generate a module ID for
757        // a target that was excluded from chunking.
758        if unused_references.contains_key(esm_ref) {
759            continue;
760        }
761        if *mutable {
762            // A mutable re-export needs a setter, which the compact form cannot express.
763            return Ok(None);
764        }
765
766        // An ordinary module gets source order from its analysis-time import map. A synthetic
767        // facade has no import map, so its explicit reference list supplies the same ordering.
768        let idx = match eval_context.imports.exports.get(exported) {
769            Some(Export::ImportedBinding(idx, _, _)) => Some(*idx),
770            Some(_) => return Ok(None),
771            None => None,
772        };
773
774        let referenced_asset =
775            ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?;
776        let Some(ReferencedAssetIdent::Module {
777            namespace_ident,
778            ctxt,
779            export: Some(imported_key),
780            import_source: ImportSource::Module { asset },
781        }) = referenced_asset
782            .get_ident(
783                chunking_context,
784                Some(imported_name.clone()),
785                scope_hoisting_context,
786            )
787            .await?
788        else {
789            // Scope-hoisted into a local binding, an external, or a namespace re-export: none of
790            // these are a property read off an imported namespace.
791            return Ok(None);
792        };
793
794        let exported_key = mangled_names
795            .and_then(|names| names.get(exported))
796            .unwrap_or(exported)
797            .clone();
798        let key = (namespace_ident.clone(), ctxt);
799        let order = if let Some(idx) = idx {
800            idx
801        } else if let Some(order) = synthetic_order.get(&key) {
802            *order
803        } else {
804            positions_known = false;
805            0
806        };
807
808        let group = groups.entry(key).or_insert_with(|| ReexportGroup {
809            order,
810            namespace_ident,
811            ctxt,
812            asset,
813            locally_bound: false,
814            reference_spans: FxHashSet::default(),
815            pairs: Vec::new(),
816        });
817        group.order = group.order.min(order);
818        if let Some(idx) = idx {
819            group.locally_bound |= locally_bound.contains(&idx);
820            group
821                .reference_spans
822                .insert(eval_context.imports.reference_span(idx));
823        }
824        group.pairs.push((exported_key, imported_key));
825    }
826
827    if groups.is_empty() {
828        return Ok(None);
829    }
830
831    let mut groups: Vec<ReexportGroup> = groups.into_values().collect();
832    groups.sort_by_key(|g| g.order);
833
834    // A scope-hoisted factory contains several logical modules. Another registration in the same
835    // factory may still read a namespace imported by this one, so suppression is only safe when
836    // this module owns its factory. Unknown relative order is safe only for one group.
837    let subsume_imports = mode == ExportRegistrationMode::Reexport
838        && scope_hoisting_context.module().is_none()
839        && (positions_known || groups.len() == 1)
840        && !groups.iter().any(|group| group.locally_bound);
841
842    Ok(Some(CompactReexports {
843        groups,
844        subsume_imports,
845    }))
846}
847
848/// Builds the `TURBOPACK_ESM_REEXPORT` call and reports which references it subsumes.
849async fn emit_compact_reexports(
850    compact: CompactReexports,
851    chunking_context: Vc<Box<dyn ChunkingContext>>,
852    scope_hoisting_context: ScopeHoistingContext<'_>,
853) -> Result<(CodeGeneration, SubsumedImports)> {
854    let mut elems: Vec<Option<ExprOrSpread>> = Vec::new();
855    let mut subsumed = SubsumedImports::default();
856
857    for (i, group) in compact.groups.iter().enumerate() {
858        // Both names of every pair are recovered by splitting on commas in the compact spelling,
859        // so select that spelling independently for each group whose names are all comma-free.
860        let comma_free = group
861            .pairs
862            .iter()
863            .all(|(a, b)| !a.contains(',') && !b.contains(','));
864
865        if i > 0 {
866            // Separates this group from the previous one.
867            elems.push(Some(Expr::Lit(Lit::Num(Number::from(0))).into()));
868        }
869
870        if compact.subsume_imports {
871            let id = group.asset.chunk_item_id(chunking_context).await?;
872            elems.push(Some(module_id_to_lit(&id).into()));
873            subsumed
874                .namespaces
875                .insert((group.namespace_ident.clone(), group.ctxt));
876            subsumed
877                .evaluation_spans
878                .extend(group.reference_spans.iter().copied());
879        } else {
880            elems.push(Some(
881                Expr::Ident(Ident::new(
882                    group.namespace_ident.clone().into(),
883                    DUMMY_SP,
884                    group.ctxt.unwrap_or_default(),
885                ))
886                .into(),
887            ));
888        }
889
890        if group.pairs.is_empty() {
891            // An evaluation-only group is represented by its head followed immediately by the
892            // next group's sentinel (or the end of the list).
893            continue;
894        }
895
896        if comma_free {
897            let joined = group
898                .pairs
899                .iter()
900                .flat_map(|(a, b)| [a.as_str(), b.as_str()])
901                .collect::<Vec<_>>()
902                .join(",");
903            elems.push(Some(Expr::Lit(Lit::Str(joined.into())).into()));
904        } else {
905            for (exported, imported) in &group.pairs {
906                elems.push(Some(Expr::Lit(Lit::Str(exported.as_str().into())).into()));
907                elems.push(Some(Expr::Lit(Lit::Str(imported.as_str().into())).into()));
908            }
909        }
910    }
911
912    let list = Expr::Array(ArrayLit {
913        span: DUMMY_SP,
914        elems,
915    });
916
917    let stmt = if let Some(module) = scope_hoisting_context.module() {
918        let id = module.chunk_item_id(chunking_context).await?;
919        quote!("$reexport($list, $id);" as Stmt,
920            reexport: Expr = TURBOPACK_ESM_REEXPORT.into(),
921            list: Expr = list,
922            id: Expr = module_id_to_lit(&id)
923        )
924    } else {
925        quote!("$reexport($list);" as Stmt,
926            reexport: Expr = TURBOPACK_ESM_REEXPORT.into(),
927            list: Expr = list
928        )
929    };
930
931    // Emitted as a normal hoisted statement so it lands after the imports that are kept -- the
932    // namespace variables it reads must already exist, and any import that was *not* subsumed must
933    // still run first.
934    Ok((
935        CodeGeneration::hoisted_stmt(rcstr!("__turbopack_esm_reexport__"), stmt),
936        subsumed,
937    ))
938}
939
940/// One source module's contribution to a compact re-export registration: the module the exports
941/// come from, and the `exported name -> name on that module` pairs taken from it.
942struct ReexportGroup {
943    /// Lowest binding-reference index among this group's exports. Binding references retain their
944    /// relative declaration order even though evaluation references occupy a separate index range.
945    order: usize,
946    /// The variable an already-generated import bound to this module's namespace.
947    namespace_ident: String,
948    ctxt: Option<SyntaxContext>,
949    /// The module itself, used when the head is emitted as a module id.
950    asset: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
951    /// Whether any reference to this module also binds a name the module's own code uses.
952    locally_bound: bool,
953    /// Source spans of the re-export declarations whose evaluation references are subsumed.
954    reference_spans: FxHashSet<Span>,
955    pairs: Vec<(RcStr, RcStr)>,
956}
957
958/// Identifies the namespace variable a group reads from. The syntax context is part of the key
959/// because two merged modules can import the same target under the same generated name in
960/// different hygiene contexts, which the import code generation also keys on -- merging those into
961/// one group would read the wrong variable and suppress both declarations.
962pub(crate) type NamespaceKey = (String, Option<SyntaxContext>);
963
964type SyntheticReexportReferences<'a> = (
965    &'a [ResolvedVc<EcmascriptModulePartReference>],
966    &'a [ResolvedVc<EsmAssetReference>],
967);
968
969struct CompactRegistrationSafety {
970    mode: ExportRegistrationMode,
971    is_circuit_breaker: bool,
972    is_async_module: bool,
973}
974
975fn compact_registration_is_safe(options: CompactRegistrationSafety) -> bool {
976    matches!(
977        options.mode,
978        ExportRegistrationMode::Mixed | ExportRegistrationMode::Reexport
979    ) && !options.is_circuit_breaker
980        && !options.is_async_module
981}
982
983/// Imports performed by one compact registration. Binding references are identified by namespace;
984/// evaluation references also need their source span so an earlier independent `import './x'` of
985/// the same module is retained rather than being suppressed with a later re-export declaration.
986#[derive(Default)]
987pub(crate) struct SubsumedImports {
988    pub namespaces: FxHashSet<NamespaceKey>,
989    pub evaluation_spans: FxHashSet<Span>,
990}
991
992/// A compact registration, ready to emit.
993struct CompactReexports {
994    groups: Vec<ReexportGroup>,
995    /// Whether the group heads are module ids (the registration performs the imports itself) or
996    /// namespace objects (the imports stay where they are).
997    subsume_imports: bool,
998}
999
1000impl EsmExports {
1001    pub(crate) async fn code_generation(
1002        self: Vc<Self>,
1003        chunking_context: Vc<Box<dyn ChunkingContext>>,
1004        scope_hoisting_context: ScopeHoistingContext<'_>,
1005        eval_context: &EvalContext,
1006        module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
1007        export_registration_mode: ExportRegistrationMode,
1008        synthetic_references: Option<SyntheticReexportReferences<'_>>,
1009        is_async_module: bool,
1010    ) -> Result<(CodeGeneration, SubsumedImports)> {
1011        let unused_references = chunking_context.unused_references().await?;
1012        let export_usage_info = chunking_context
1013            .module_export_usage(*ResolvedVc::upcast(module))
1014            .await?;
1015        let expanded = self.expand_exports(*export_usage_info.export_usage).await?;
1016
1017        if scope_hoisting_context.skip_module_exports() && expanded.dynamic_exports.is_empty() {
1018            // If the current module is not exposed, no need to generate exports.
1019            //
1020            // If there are dynamic_exports, we still need to export everything because it wasn't
1021            // possible to determine statically where a reexport is coming from which will instead
1022            // be handled at runtime via property access, e.g. `export * from "./some-dynamic-cjs"`
1023            return Ok((CodeGeneration::empty(), Default::default()));
1024        }
1025
1026        let mut dynamic_exports = Vec::<Box<Expr>>::new();
1027        {
1028            let id = if let Some(module) = scope_hoisting_context.module()
1029                && !expanded.dynamic_exports.is_empty()
1030            {
1031                Some(module.chunk_item_id(chunking_context).await?)
1032            } else {
1033                None
1034            };
1035
1036            for dynamic_export_asset in &expanded.dynamic_exports {
1037                let ident = ReferencedAsset::get_ident_from_placeable(
1038                    dynamic_export_asset,
1039                    chunking_context,
1040                )
1041                .await?;
1042
1043                if let Some(id) = &id {
1044                    dynamic_exports.push(quote_expr!(
1045                        "$turbopack_dynamic($arg, $id)",
1046                        turbopack_dynamic: Expr = TURBOPACK_DYNAMIC.into(),
1047                        arg: Expr = Ident::new(ident.into(), DUMMY_SP, Default::default()).into(),
1048                        id: Expr = module_id_to_lit(id)
1049                    ));
1050                } else {
1051                    dynamic_exports.push(quote_expr!(
1052                        "$turbopack_dynamic($arg)",
1053                        turbopack_dynamic: Expr = TURBOPACK_DYNAMIC.into(),
1054                        arg: Expr = Ident::new(ident.into(), DUMMY_SP, Default::default()).into()
1055                    ));
1056                }
1057            }
1058        }
1059
1060        #[derive(Eq, PartialEq)]
1061        enum ExportBinding {
1062            Getter(Expr),
1063            GetterSetter(Expr, Expr),
1064            Value(Expr),
1065            None,
1066        }
1067
1068        let mut getters = Vec::new();
1069        // The keys this module's exports are emitted under. Consumers resolve the same map for this
1070        // module (see `ReferencedAsset::get_ident_inner`), so both sides always agree.
1071        let mangled_names = mangled_export_names(*module, chunking_context).await?;
1072
1073        // A module whose exports are *only* forwarded from other modules can register them all in
1074        // one compact call instead of one arrow function per binding. `export_registration_mode`
1075        // decided during analysis whether that is possible without reordering evaluation. Async
1076        // modules may yield a promise from `i()`. A known circuit breaker must expose its getters
1077        // before importing the other side of the cycle, and a module without analysis must do the
1078        // same conservatively, so all three stay on the general registration.
1079        let compact = if compact_registration_is_safe(CompactRegistrationSafety {
1080            mode: export_registration_mode,
1081            is_circuit_breaker: export_usage_info.is_circuit_breaker,
1082            is_async_module,
1083        }) && expanded.dynamic_exports.is_empty()
1084            && !expanded.exports.is_empty()
1085        {
1086            build_compact_reexports(
1087                &expanded.exports,
1088                mangled_names.as_ref(),
1089                eval_context,
1090                export_registration_mode,
1091                synthetic_references,
1092                chunking_context,
1093                scope_hoisting_context,
1094            )
1095            .await?
1096        } else {
1097            None
1098        };
1099
1100        if let Some(compact) = compact {
1101            return emit_compact_reexports(compact, chunking_context, scope_hoisting_context).await;
1102        }
1103
1104        for (exported, local) in &expanded.exports {
1105            if let EsmExport::ImportedBinding(reference, ..)
1106            | EsmExport::ImportedNamespace(reference) = local
1107                && unused_references.contains_key(reference)
1108            {
1109                continue;
1110            }
1111            let exprs: ExportBinding = match local {
1112                EsmExport::Error => ExportBinding::Getter(quote!(
1113                    "(() => { throw new Error(\"Failed binding. See build errors!\"); })" as Expr,
1114                )),
1115                EsmExport::LocalBinding(name, liveness) => {
1116                    // TODO ideally, this information would just be stored in
1117                    // EsmExport::LocalBinding and we wouldn't have to re-correlated this
1118                    // information with eval_context.imports.exports to get the syntax context.
1119                    let binding = if let Some((local, ctxt)) = eval_context
1120                        .imports
1121                        .exports_ids
1122                        .get(exported)
1123                        .map(|(id, _)| id)
1124                    {
1125                        Some((local.clone(), *ctxt))
1126                    } else {
1127                        bail!(
1128                            "Expected export to be in eval context {:?} {:?}",
1129                            exported,
1130                            eval_context.imports,
1131                        )
1132                    };
1133                    let (local, ctxt) = binding.unwrap_or_else(|| {
1134                        // Fallback, shouldn't happen in practice
1135                        (
1136                            if name == "default" {
1137                                MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM.clone()
1138                            } else {
1139                                name.as_str().into()
1140                            },
1141                            SyntaxContext::empty(),
1142                        )
1143                    });
1144
1145                    let local = Ident::new(local, DUMMY_SP, ctxt);
1146                    match (liveness, export_usage_info.is_circuit_breaker) {
1147                        (Liveness::Constant, false) => ExportBinding::Value(Expr::Ident(local)),
1148                        // If the value might change or we are a circuit breaker we must bind a
1149                        // getter to avoid capturing the value at the wrong time.
1150                        (Liveness::Live, _) | (Liveness::Constant, true) => {
1151                            ExportBinding::Getter(quote!("() => $local" as Expr, local = local))
1152                        }
1153                        (Liveness::Mutable, _) => ExportBinding::GetterSetter(
1154                            quote!("() => $local" as Expr, local = local.clone()),
1155                            quote!(
1156                                "($new) => $local = $new" as Expr,
1157                                local: AssignTarget = AssignTarget::Simple(local.into()),
1158                                new = Ident::new(format!("new_{name}").into(), DUMMY_SP, ctxt),
1159                            ),
1160                        ),
1161                    }
1162                }
1163                EsmExport::ImportedBinding(esm_ref, name, mutable) => {
1164                    let referenced_asset =
1165                        ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?;
1166                    referenced_asset
1167                        .get_ident(chunking_context, Some(name.clone()), scope_hoisting_context)
1168                        .await?
1169                        .map(|ident| {
1170                            let expr = ident.as_expr_individual(DUMMY_SP);
1171                            let read_expr = expr.map_either(Expr::from, Expr::from).into_inner();
1172                            use crate::references::esm::base::ReferencedAssetIdent;
1173                            match &ident {
1174                                ReferencedAssetIdent::LocalBinding {ctxt, liveness,.. } => {
1175                                    debug_assert!(*mutable == (*liveness == Liveness::Mutable), "If the re-export is mutable, the merged local must be too");
1176                                    // If we are re-exporting something but got merged with it we can treat it like a local export
1177                                     match (liveness, export_usage_info.is_circuit_breaker) {
1178                                        (Liveness::Constant, false) => {
1179                                            ExportBinding::Value(read_expr)
1180                                        }
1181                                        // If the value might change or we are a circuit breaker we must bind a
1182                                        // getter to avoid capturing the value at the wrong time.
1183                                        (Liveness::Live, _) | (Liveness::Constant, true) => {
1184                                            // In the constant case, we could still export as a value if we knew that the module
1185                                            // came _before_ us, but we don't at this point.
1186                                            ExportBinding::Getter(quote!("() => $local" as Expr, local: Expr = read_expr))
1187                                        }
1188                                        (Liveness::Mutable, _) => {
1189                                            let assign_target = AssignTarget::Simple(
1190                                                        ident.as_expr_individual(DUMMY_SP).map_either(|i| SimpleAssignTarget::Ident(i.into()), SimpleAssignTarget::Member).into_inner());
1191                                            ExportBinding::GetterSetter(
1192                                                quote!("() => $local" as Expr, local: Expr= read_expr.clone()),
1193                                                quote!(
1194                                                    "($new) => $lhs = $new" as Expr,
1195                                                    lhs: AssignTarget = assign_target,
1196                                                    new = Ident::new(format!("new_{name}").into(), DUMMY_SP, *ctxt),
1197                                                )
1198                                            )
1199                                        }
1200                                    }
1201                                },
1202                                ReferencedAssetIdent::Module { .. } => {
1203                                    // Otherwise we need to bind as a getter to preserve the 'liveness' of the other modules bindings.
1204                                    // TODO: If this becomes important it might be faster to use the runtime to copy PropertyDescriptors across modules
1205                                    // since that would reduce allocations and optimize access. We could do this by passing the module-id up.
1206                                    let getter = quote!("() => $expr" as Expr, expr: Expr = read_expr);
1207                                    let assign_target = AssignTarget::Simple(
1208                                                    ident.as_expr_individual(DUMMY_SP).map_either(|i| SimpleAssignTarget::Ident(i.into()), SimpleAssignTarget::Member).into_inner());
1209                                    if *mutable {
1210                                        ExportBinding::GetterSetter(
1211                                            getter,
1212                                            quote!(
1213                                                "($new) => $lhs = $new" as Expr,
1214                                                lhs: AssignTarget = assign_target,
1215                                                new = Ident::new(
1216                                                    format!("new_{name}").into(),
1217                                                    DUMMY_SP,
1218                                                    Default::default()
1219                                                ),
1220                                            ))
1221                                    } else {
1222                                        ExportBinding::Getter(getter)
1223                                    }
1224                                }
1225                            }
1226                        }).unwrap_or(ExportBinding::None)
1227                }
1228                EsmExport::ImportedNamespace(esm_ref) => {
1229                    let referenced_asset =
1230                        ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?;
1231                    referenced_asset
1232                        .get_ident(chunking_context, None, scope_hoisting_context)
1233                        .await?
1234                        .map(|ident| {
1235                            let imported = ident.as_expr(DUMMY_SP, false);
1236                            if export_usage_info.is_circuit_breaker {
1237                                ExportBinding::Getter(quote!(
1238                                    "(() => $imported)" as Expr,
1239                                    imported: Expr = imported
1240                                ))
1241                            } else {
1242                                ExportBinding::Value(imported)
1243                            }
1244                        })
1245                        .unwrap_or(ExportBinding::None)
1246                }
1247            };
1248            if exprs != ExportBinding::None {
1249                getters.push(Some(
1250                    Expr::Lit(Lit::Str(Str {
1251                        span: DUMMY_SP,
1252                        // The key this export is emitted under: the mangled one when this module's
1253                        // names are shortened, otherwise the original.
1254                        value: mangled_names
1255                            .as_ref()
1256                            .and_then(|names| names.get(exported))
1257                            .unwrap_or(exported)
1258                            .as_str()
1259                            .into(),
1260                        raw: None,
1261                    }))
1262                    .into(),
1263                ));
1264                match exprs {
1265                    // Accessors carry the discriminator, values do not. Values are by far the
1266                    // common case, and tagging the accessors is still unambiguous: the tag is
1267                    // always followed by a function, while a value of `0` is followed by the next
1268                    // binding's name or by the end of the array.
1269                    ExportBinding::Getter(getter) => {
1270                        getters.push(Some(Expr::Lit(Lit::Num(Number::from(0))).into()));
1271                        getters.push(Some(getter.into()));
1272                    }
1273                    ExportBinding::GetterSetter(getter, setter) => {
1274                        getters.push(Some(Expr::Lit(Lit::Num(Number::from(0))).into()));
1275                        getters.push(Some(getter.into()));
1276                        getters.push(Some(setter.into()));
1277                    }
1278                    ExportBinding::Value(value) => {
1279                        getters.push(Some(value.into()));
1280                    }
1281                    ExportBinding::None => {}
1282                };
1283            }
1284        }
1285        let getters = Expr::Array(ArrayLit {
1286            span: DUMMY_SP,
1287            elems: getters,
1288        });
1289        let dynamic_stmt = if !dynamic_exports.is_empty() {
1290            vec![CodeGenerationHoistedStmt::new(
1291                rcstr!("__turbopack_dynamic__"),
1292                Stmt::Expr(ExprStmt {
1293                    span: DUMMY_SP,
1294                    expr: Expr::from_exprs(dynamic_exports),
1295                }),
1296            )]
1297        } else {
1298            vec![]
1299        };
1300
1301        // When a module has dynamic re-exports (`export *` from a module whose
1302        // exports are only known at runtime), its namespace object must stay
1303        // extensible so the dynamic export proxy can surface those keys. Signal
1304        // that to the runtime so it skips sealing the namespace.
1305        let has_dynamic_exports = !expanded.dynamic_exports.is_empty();
1306        let esm_exports = vec![CodeGenerationHoistedStmt::new(
1307            rcstr!("__turbopack_esm__"),
1308            if let Some(module) = scope_hoisting_context.module() {
1309                let id = module.chunk_item_id(chunking_context).await?;
1310                if has_dynamic_exports {
1311                    quote!("$turbopack_esm($getters, $id, true);" as Stmt,
1312                        turbopack_esm: Expr = TURBOPACK_ESM.into(),
1313                        getters: Expr = getters,
1314                        id: Expr = module_id_to_lit(&id)
1315                    )
1316                } else {
1317                    quote!("$turbopack_esm($getters, $id);" as Stmt,
1318                        turbopack_esm: Expr = TURBOPACK_ESM.into(),
1319                        getters: Expr = getters,
1320                        id: Expr = module_id_to_lit(&id)
1321                    )
1322                }
1323            } else if has_dynamic_exports {
1324                quote!("$turbopack_esm($getters, undefined, true);" as Stmt,
1325                    turbopack_esm: Expr = TURBOPACK_ESM.into(),
1326                    getters: Expr = getters
1327                )
1328            } else {
1329                quote!("$turbopack_esm($getters);" as Stmt,
1330                    turbopack_esm: Expr = TURBOPACK_ESM.into(),
1331                    getters: Expr = getters
1332                )
1333            },
1334        )];
1335        // If we are a circuit breaker module we need to expose exports first so they are available
1336        // to a cyclic importer otherwise we put them at the bottom of the module factory.
1337        Ok(if export_usage_info.is_circuit_breaker {
1338            (
1339                CodeGeneration::new(vec![], dynamic_stmt, esm_exports, vec![], vec![]),
1340                Default::default(),
1341            )
1342        } else {
1343            (
1344                CodeGeneration::new(vec![], vec![], vec![], dynamic_stmt, esm_exports),
1345                Default::default(),
1346            )
1347        })
1348    }
1349}
1350
1351#[cfg(test)]
1352mod tests {
1353    use super::{CompactRegistrationSafety, ExportRegistrationMode, compact_registration_is_safe};
1354
1355    #[test]
1356    fn compact_registration_rejects_async_modules_and_cycle_breakers() {
1357        assert!(compact_registration_is_safe(CompactRegistrationSafety {
1358            mode: ExportRegistrationMode::Reexport,
1359            is_circuit_breaker: false,
1360            is_async_module: false,
1361        }));
1362        assert!(!compact_registration_is_safe(CompactRegistrationSafety {
1363            mode: ExportRegistrationMode::Reexport,
1364            is_circuit_breaker: false,
1365            is_async_module: true,
1366        }));
1367        // A known cycle breaker and the conservative unknown-analysis fallback both set this flag.
1368        assert!(!compact_registration_is_safe(CompactRegistrationSafety {
1369            mode: ExportRegistrationMode::Reexport,
1370            is_circuit_breaker: true,
1371            is_async_module: false,
1372        }));
1373        assert!(!compact_registration_is_safe(CompactRegistrationSafety {
1374            mode: ExportRegistrationMode::Normal,
1375            is_circuit_breaker: false,
1376            is_async_module: false,
1377        }));
1378    }
1379}