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#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, NonLocalValue, Encode, Decode)]
51pub enum Liveness {
52 Constant,
54 Live,
56 Mutable,
60}
61
62#[derive(Clone, Hash, Debug, PartialEq, Eq, NonLocalValue, Encode, Decode)]
63pub enum EsmExport {
64 LocalBinding(RcStr, Liveness),
68 ImportedBinding(ResolvedVc<Box<dyn ModuleReference>>, RcStr, bool),
72 ImportedNamespace(ResolvedVc<Box<dyn ModuleReference>>),
74 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 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 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 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 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 (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 #[bincode(with = "turbo_bincode::indexmap")]
354 esm_exports: FxIndexMap<RcStr, EsmExport>,
355 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 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 pub exports: FrozenMap<RcStr, EsmExport>,
540 pub star_exports: Vec<ResolvedVc<Box<dyn ModuleReference>>>,
542 pub mangle_export_names: bool,
546}
547
548#[turbo_tasks::value(shared)]
553#[derive(Hash, Debug)]
554pub struct ExpandedExports {
555 pub exports: FrozenMap<RcStr, EsmExport>,
556 pub dynamic_exports: Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
558}
559
560#[turbo_tasks::value_impl]
561impl EsmExports {
562 #[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 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 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 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
648async 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
670async 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 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 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 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 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 if unused_references.contains_key(esm_ref) {
759 continue;
760 }
761 if *mutable {
762 return Ok(None);
764 }
765
766 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 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 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
848async 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 let comma_free = group
861 .pairs
862 .iter()
863 .all(|(a, b)| !a.contains(',') && !b.contains(','));
864
865 if i > 0 {
866 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 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 Ok((
935 CodeGeneration::hoisted_stmt(rcstr!("__turbopack_esm_reexport__"), stmt),
936 subsumed,
937 ))
938}
939
940struct ReexportGroup {
943 order: usize,
946 namespace_ident: String,
948 ctxt: Option<SyntaxContext>,
949 asset: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
951 locally_bound: bool,
953 reference_spans: FxHashSet<Span>,
955 pairs: Vec<(RcStr, RcStr)>,
956}
957
958pub(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#[derive(Default)]
987pub(crate) struct SubsumedImports {
988 pub namespaces: FxHashSet<NamespaceKey>,
989 pub evaluation_spans: FxHashSet<Span>,
990}
991
992struct CompactReexports {
994 groups: Vec<ReexportGroup>,
995 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 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 let mangled_names = mangled_export_names(*module, chunking_context).await?;
1072
1073 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 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 (
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 (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 match (liveness, export_usage_info.is_circuit_breaker) {
1178 (Liveness::Constant, false) => {
1179 ExportBinding::Value(read_expr)
1180 }
1181 (Liveness::Live, _) | (Liveness::Constant, true) => {
1184 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 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 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 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 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 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 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}