1use std::{borrow::Cow, collections::BTreeMap, ops::ControlFlow};
2
3use anyhow::Result;
4use rustc_hash::FxHashSet;
5use serde::{Deserialize, Serialize};
6use swc_core::{
7 common::DUMMY_SP,
8 ecma::ast::{
9 AssignTarget, ComputedPropName, Expr, ExprStmt, Ident, KeyValueProp, Lit, MemberExpr,
10 MemberProp, ObjectLit, Prop, PropName, PropOrSpread, SimpleAssignTarget, Stmt, Str,
11 },
12 quote, quote_expr,
13};
14use turbo_rcstr::RcStr;
15use turbo_tasks::{
16 FxIndexMap, FxIndexSet, NonLocalValue, ResolvedVc, TryFlatJoinIterExt, ValueToString, Vc,
17 trace::TraceRawVcs,
18};
19use turbo_tasks_fs::glob::Glob;
20use turbopack_core::{
21 chunk::ChunkingContext,
22 ident::AssetIdent,
23 issue::{IssueExt, IssueSeverity, StyledString, analyze::AnalyzeIssue},
24 module::Module,
25 module_graph::ModuleGraph,
26 reference::ModuleReference,
27 resolve::ModulePart,
28};
29
30use super::base::ReferencedAsset;
31use crate::{
32 EcmascriptModuleAsset,
33 chunk::{EcmascriptChunkPlaceable, EcmascriptExports},
34 code_gen::{CodeGeneration, CodeGenerationHoistedStmt},
35 magic_identifier,
36 parse::ParseResult,
37 runtime_functions::{TURBOPACK_DYNAMIC, TURBOPACK_ESM},
38 tree_shake::asset::EcmascriptModulePartAsset,
39};
40
41#[derive(Clone, Hash, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue)]
42pub enum EsmExport {
43 LocalBinding(RcStr, bool),
47 ImportedBinding(ResolvedVc<Box<dyn ModuleReference>>, RcStr, bool),
51 ImportedNamespace(ResolvedVc<Box<dyn ModuleReference>>),
53 Error,
55}
56
57#[turbo_tasks::function]
58pub async fn is_export_missing(
59 module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
60 export_name: RcStr,
61) -> Result<Vc<bool>> {
62 if export_name == "__turbopack_module_id__" {
63 return Ok(Vc::cell(false));
64 }
65
66 let exports = module.get_exports().await?;
67 let exports = match &*exports {
68 EcmascriptExports::None => return Ok(Vc::cell(true)),
69 EcmascriptExports::Value => return Ok(Vc::cell(false)),
70 EcmascriptExports::CommonJs => return Ok(Vc::cell(false)),
71 EcmascriptExports::EmptyCommonJs => return Ok(Vc::cell(export_name != "default")),
72 EcmascriptExports::DynamicNamespace => return Ok(Vc::cell(false)),
73 EcmascriptExports::EsmExports(exports) => *exports,
74 };
75
76 let exports = exports.await?;
77 if exports.exports.contains_key(&export_name) {
78 return Ok(Vc::cell(false));
79 }
80 if export_name == "default" {
81 return Ok(Vc::cell(true));
82 }
83
84 if exports.star_exports.is_empty() {
85 return Ok(Vc::cell(true));
86 }
87
88 let all_export_names = get_all_export_names(*module).await?;
89 if all_export_names.esm_exports.contains_key(&export_name) {
90 return Ok(Vc::cell(false));
91 }
92
93 for &dynamic_module in &all_export_names.dynamic_exporting_modules {
94 let exports = dynamic_module.get_exports().await?;
95 match &*exports {
96 EcmascriptExports::Value
97 | EcmascriptExports::CommonJs
98 | EcmascriptExports::DynamicNamespace => {
99 return Ok(Vc::cell(false));
100 }
101 EcmascriptExports::None
102 | EcmascriptExports::EmptyCommonJs
103 | EcmascriptExports::EsmExports(_) => {}
104 }
105 }
106
107 Ok(Vc::cell(true))
108}
109
110#[turbo_tasks::function]
111pub async fn all_known_export_names(
112 module: Vc<Box<dyn EcmascriptChunkPlaceable>>,
113) -> Result<Vc<Vec<RcStr>>> {
114 let export_names = get_all_export_names(module).await?;
115 Ok(Vc::cell(export_names.esm_exports.keys().cloned().collect()))
116}
117
118#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue)]
119pub enum FoundExportType {
120 Found,
121 Dynamic,
122 NotFound,
123 SideEffects,
124 Unknown,
125}
126
127#[turbo_tasks::value]
128pub struct FollowExportsResult {
129 pub module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
130 pub export_name: Option<RcStr>,
131 pub ty: FoundExportType,
132}
133
134#[turbo_tasks::function]
135pub async fn follow_reexports(
136 module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
137 export_name: RcStr,
138 side_effect_free_packages: Vc<Glob>,
139 ignore_side_effect_of_entry: bool,
140) -> Result<Vc<FollowExportsResult>> {
141 if !ignore_side_effect_of_entry
142 && !*module
143 .is_marked_as_side_effect_free(side_effect_free_packages)
144 .await?
145 {
146 return Ok(FollowExportsResult::cell(FollowExportsResult {
147 module,
148 export_name: Some(export_name),
149 ty: FoundExportType::SideEffects,
150 }));
151 }
152 let mut module = module;
153 let mut export_name = export_name;
154 loop {
155 let exports = module.get_exports().await?;
156 let EcmascriptExports::EsmExports(exports) = &*exports else {
157 return Ok(FollowExportsResult::cell(FollowExportsResult {
158 module,
159 export_name: Some(export_name),
160 ty: FoundExportType::Dynamic,
161 }));
162 };
163
164 let exports_ref = exports.await?;
166 if let Some(export) = exports_ref.exports.get(&export_name) {
167 match handle_declared_export(module, export_name, export, side_effect_free_packages)
168 .await?
169 {
170 ControlFlow::Continue((m, n)) => {
171 module = m.to_resolved().await?;
172 export_name = n;
173 continue;
174 }
175 ControlFlow::Break(result) => {
176 return Ok(result.cell());
177 }
178 }
179 }
180
181 if !exports_ref.star_exports.is_empty() && &*export_name != "default" {
183 let result = find_export_from_reexports(*module, export_name.clone()).await?;
184 if let Some(m) = result.esm_export {
185 module = m;
186 continue;
187 }
188 return match &result.dynamic_exporting_modules[..] {
189 [] => Ok(FollowExportsResult {
190 module,
191 export_name: Some(export_name),
192 ty: FoundExportType::NotFound,
193 }
194 .cell()),
195 [module] => Ok(FollowExportsResult {
196 module: *module,
197 export_name: Some(export_name),
198 ty: FoundExportType::Dynamic,
199 }
200 .cell()),
201 _ => Ok(FollowExportsResult {
202 module,
203 export_name: Some(export_name),
204 ty: FoundExportType::Dynamic,
205 }
206 .cell()),
207 };
208 }
209
210 return Ok(FollowExportsResult::cell(FollowExportsResult {
211 module,
212 export_name: Some(export_name),
213 ty: FoundExportType::NotFound,
214 }));
215 }
216}
217
218async fn handle_declared_export(
219 module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
220 export_name: RcStr,
221 export: &EsmExport,
222 side_effect_free_packages: Vc<Glob>,
223) -> Result<ControlFlow<FollowExportsResult, (Vc<Box<dyn EcmascriptChunkPlaceable>>, RcStr)>> {
224 match export {
225 EsmExport::ImportedBinding(reference, name, _) => {
226 if let ReferencedAsset::Some(module) =
227 *ReferencedAsset::from_resolve_result(reference.resolve_reference()).await?
228 {
229 if !*module
230 .is_marked_as_side_effect_free(side_effect_free_packages)
231 .await?
232 {
233 return Ok(ControlFlow::Break(FollowExportsResult {
234 module,
235 export_name: Some(name.clone()),
236 ty: FoundExportType::SideEffects,
237 }));
238 }
239 return Ok(ControlFlow::Continue((*module, name.clone())));
240 }
241 }
242 EsmExport::ImportedNamespace(reference) => {
243 if let ReferencedAsset::Some(module) =
244 *ReferencedAsset::from_resolve_result(reference.resolve_reference()).await?
245 {
246 return Ok(ControlFlow::Break(FollowExportsResult {
247 module,
248 export_name: None,
249 ty: FoundExportType::Found,
250 }));
251 }
252 }
253 EsmExport::LocalBinding(..) => {
254 return Ok(ControlFlow::Break(FollowExportsResult {
255 module,
256 export_name: Some(export_name),
257 ty: FoundExportType::Found,
258 }));
259 }
260 EsmExport::Error => {
261 return Ok(ControlFlow::Break(FollowExportsResult {
262 module,
263 export_name: Some(export_name),
264 ty: FoundExportType::Unknown,
265 }));
266 }
267 }
268 Ok(ControlFlow::Break(FollowExportsResult {
269 module,
270 export_name: Some(export_name),
271 ty: FoundExportType::Unknown,
272 }))
273}
274
275#[turbo_tasks::value]
276struct FindExportFromReexportsResult {
277 esm_export: Option<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
278 dynamic_exporting_modules: Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
279}
280
281#[turbo_tasks::function]
282async fn find_export_from_reexports(
283 module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
284 export_name: RcStr,
285) -> Result<Vc<FindExportFromReexportsResult>> {
286 if let Some(module) =
287 Vc::try_resolve_downcast_type::<EcmascriptModulePartAsset>(*module).await?
288 {
289 if matches!(module.await?.part, ModulePart::Exports) {
290 let module_part = EcmascriptModulePartAsset::select_part(
291 *module.await?.full_module,
292 ModulePart::export(export_name.clone()),
293 );
294
295 if (Vc::try_resolve_downcast_type::<EcmascriptModuleAsset>(module_part).await?)
298 .is_none()
299 {
300 return Ok(find_export_from_reexports(
301 Vc::upcast(module_part),
302 export_name,
303 ));
304 }
305 }
306 }
307
308 let all_export_names = get_all_export_names(*module).await?;
309 let esm_export = all_export_names.esm_exports.get(&export_name).copied();
310 Ok(FindExportFromReexportsResult {
311 esm_export,
312 dynamic_exporting_modules: all_export_names.dynamic_exporting_modules.clone(),
313 }
314 .cell())
315}
316
317#[turbo_tasks::value]
318struct AllExportNamesResult {
319 esm_exports: FxIndexMap<RcStr, ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
320 dynamic_exporting_modules: Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
321}
322
323#[turbo_tasks::function]
324async fn get_all_export_names(
325 module: ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>,
326) -> Result<Vc<AllExportNamesResult>> {
327 let exports = module.get_exports().await?;
328 let EcmascriptExports::EsmExports(exports) = &*exports else {
329 return Ok(AllExportNamesResult {
330 esm_exports: FxIndexMap::default(),
331 dynamic_exporting_modules: vec![module],
332 }
333 .cell());
334 };
335
336 let exports = exports.await?;
337 let mut esm_exports = FxIndexMap::default();
338 let mut dynamic_exporting_modules = Vec::new();
339 esm_exports.extend(exports.exports.keys().cloned().map(|n| (n, module)));
340 let star_export_names = exports
341 .star_exports
342 .iter()
343 .map(|esm_ref| async {
344 Ok(
345 if let ReferencedAsset::Some(m) =
346 *ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?
347 {
348 Some(get_all_export_names(*m))
349 } else {
350 None
351 },
352 )
353 })
354 .try_flat_join()
355 .await?;
356 for star_export_names in star_export_names {
357 let star_export_names = star_export_names.await?;
358 esm_exports.extend(
359 star_export_names
360 .esm_exports
361 .iter()
362 .map(|(k, &v)| (k.clone(), v)),
363 );
364 dynamic_exporting_modules
365 .extend(star_export_names.dynamic_exporting_modules.iter().copied());
366 }
367
368 Ok(AllExportNamesResult {
369 esm_exports,
370 dynamic_exporting_modules,
371 }
372 .cell())
373}
374
375#[turbo_tasks::value]
376pub struct ExpandStarResult {
377 pub star_exports: Vec<RcStr>,
378 pub has_dynamic_exports: bool,
379}
380
381#[turbo_tasks::function]
382pub async fn expand_star_exports(
383 root_module: Vc<Box<dyn EcmascriptChunkPlaceable>>,
384) -> Result<Vc<ExpandStarResult>> {
385 let mut set = FxIndexSet::default();
386 let mut has_dynamic_exports = false;
387 let mut checked_modules = FxHashSet::default();
388 checked_modules.insert(root_module);
389 let mut queue = vec![(root_module, root_module.get_exports())];
390 while let Some((asset, exports)) = queue.pop() {
391 match &*exports.await? {
392 EcmascriptExports::EsmExports(exports) => {
393 let exports = exports.await?;
394 set.extend(exports.exports.keys().filter(|n| *n != "default").cloned());
395 for esm_ref in exports.star_exports.iter() {
396 if let ReferencedAsset::Some(asset) =
397 &*ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?
398 {
399 if checked_modules.insert(**asset) {
400 queue.push((**asset, asset.get_exports()));
401 }
402 }
403 }
404 }
405 EcmascriptExports::None | EcmascriptExports::EmptyCommonJs => {
406 emit_star_exports_issue(
407 asset.ident(),
408 format!(
409 "export * used with module {} which has no exports\nTypescript only: Did \
410 you want to export only types with `export type * from \"...\"`?\nNote: \
411 Using `export type` is more efficient than `export *` as it won't emit \
412 any runtime code.",
413 asset.ident().to_string().await?
414 )
415 .into(),
416 )
417 .await?
418 }
419 EcmascriptExports::Value => {
420 emit_star_exports_issue(
421 asset.ident(),
422 format!(
423 "export * used with module {} which only has a default export (default \
424 export is not exported with export *)\nDid you want to use `export {{ \
425 default }} from \"...\";` instead?",
426 asset.ident().to_string().await?
427 )
428 .into(),
429 )
430 .await?
431 }
432 EcmascriptExports::CommonJs => {
433 has_dynamic_exports = true;
434 emit_star_exports_issue(
435 asset.ident(),
436 format!(
437 "export * used with module {} which is a CommonJS module with exports \
438 only available at runtime\nList all export names manually (`export {{ a, \
439 b, c }} from \"...\") or rewrite the module to ESM, to avoid the \
440 additional runtime code.`",
441 asset.ident().to_string().await?
442 )
443 .into(),
444 )
445 .await?;
446 }
447 EcmascriptExports::DynamicNamespace => {
448 has_dynamic_exports = true;
449 }
450 }
451 }
452
453 Ok(ExpandStarResult {
454 star_exports: set.into_iter().collect(),
455 has_dynamic_exports,
456 }
457 .cell())
458}
459
460async fn emit_star_exports_issue(source_ident: Vc<AssetIdent>, message: RcStr) -> Result<()> {
461 AnalyzeIssue::new(
462 IssueSeverity::Warning,
463 source_ident,
464 Vc::cell("unexpected export *".into()),
465 StyledString::Text(message).cell(),
466 None,
467 None,
468 )
469 .to_resolved()
470 .await?
471 .emit();
472 Ok(())
473}
474
475#[turbo_tasks::value(shared)]
476#[derive(Hash, Debug)]
477pub struct EsmExports {
478 pub exports: BTreeMap<RcStr, EsmExport>,
479 pub star_exports: Vec<ResolvedVc<Box<dyn ModuleReference>>>,
480}
481
482#[turbo_tasks::value(shared)]
487#[derive(Hash, Debug)]
488pub struct ExpandedExports {
489 pub exports: BTreeMap<RcStr, EsmExport>,
490 pub dynamic_exports: Vec<ResolvedVc<Box<dyn EcmascriptChunkPlaceable>>>,
492}
493
494#[turbo_tasks::value_impl]
495impl EsmExports {
496 #[turbo_tasks::function]
497 pub async fn expand_exports(&self) -> Result<Vc<ExpandedExports>> {
498 let mut exports: BTreeMap<RcStr, EsmExport> = self.exports.clone();
499 let mut dynamic_exports = vec![];
500
501 for &esm_ref in self.star_exports.iter() {
502 let ReferencedAsset::Some(asset) =
505 &*ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?
506 else {
507 continue;
508 };
509
510 let export_info = expand_star_exports(**asset).await?;
511
512 for export in &export_info.star_exports {
513 if !exports.contains_key(export) {
514 exports.insert(
515 export.clone(),
516 EsmExport::ImportedBinding(
517 ResolvedVc::upcast(esm_ref),
518 export.clone(),
519 false,
520 ),
521 );
522 }
523 }
524
525 if export_info.has_dynamic_exports {
526 dynamic_exports.push(*asset);
527 }
528 }
529
530 Ok(ExpandedExports {
531 exports,
532 dynamic_exports,
533 }
534 .cell())
535 }
536}
537
538impl EsmExports {
539 pub async fn code_generation(
540 self: Vc<Self>,
541 _module_graph: Vc<ModuleGraph>,
542 chunking_context: Vc<Box<dyn ChunkingContext>>,
543 parsed: Option<Vc<ParseResult>>,
544 ) -> Result<CodeGeneration> {
545 let expanded = self.expand_exports().await?;
546 let parsed = if let Some(parsed) = parsed {
547 Some(parsed.await?)
548 } else {
549 None
550 };
551
552 let mut dynamic_exports = Vec::<Box<Expr>>::new();
553 for dynamic_export_asset in &expanded.dynamic_exports {
554 let ident =
555 ReferencedAsset::get_ident_from_placeable(dynamic_export_asset, chunking_context)
556 .await?;
557
558 dynamic_exports.push(quote_expr!(
559 "$turbopack_dynamic($arg)",
560 turbopack_dynamic: Expr = TURBOPACK_DYNAMIC.into(),
561 arg: Expr = Ident::new(ident.into(), DUMMY_SP, Default::default()).into()
562 ));
563 }
564
565 let mut props = Vec::new();
566 for (exported, local) in &expanded.exports {
567 let expr = match local {
568 EsmExport::Error => Some(quote!(
569 "(() => { throw new Error(\"Failed binding. See build errors!\"); })" as Expr,
570 )),
571 EsmExport::LocalBinding(name, mutable) => {
572 let local = if name == "default" {
573 Cow::Owned(magic_identifier::mangle("default export"))
574 } else {
575 Cow::Borrowed(name.as_str())
576 };
577 let ctxt = parsed
578 .as_ref()
579 .and_then(|parsed| {
580 if let ParseResult::Ok { eval_context, .. } = &**parsed {
581 eval_context.imports.exports.get(name).map(|id| id.1)
582 } else {
583 None
584 }
585 })
586 .unwrap_or_default();
587
588 if *mutable {
589 Some(quote!(
590 "([() => $local, ($new) => $local = $new])" as Expr,
591 local = Ident::new(local.into(), DUMMY_SP, ctxt),
592 new = Ident::new(format!("new_{name}").into(), DUMMY_SP, ctxt),
593 ))
594 } else {
595 Some(quote!(
596 "(() => $local)" as Expr,
597 local = Ident::new((name as &str).into(), DUMMY_SP, ctxt)
598 ))
599 }
600 }
601 EsmExport::ImportedBinding(esm_ref, name, mutable) => {
602 let referenced_asset =
603 ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?;
604 referenced_asset.get_ident(
605 chunking_context
606 ).await?.map(|ident| {
607 let expr = MemberExpr {
608 span: DUMMY_SP,
609 obj: Box::new(Expr::Ident(Ident::new(
610 ident.into(),
611 DUMMY_SP,
612 Default::default(),
613 ))),
614 prop: MemberProp::Computed(ComputedPropName {
615 span: DUMMY_SP,
616 expr: Box::new(Expr::Lit(Lit::Str(Str {
617 span: DUMMY_SP,
618 value: (name as &str).into(),
619 raw: None,
620 }))),
621 }),
622 };
623 if *mutable {
624 quote!(
625 "([() => $expr, ($new) => $lhs = $new])" as Expr,
626 expr: Expr = Expr::Member(expr.clone()),
627 lhs: AssignTarget = AssignTarget::Simple(SimpleAssignTarget::Member(expr)),
628 new = Ident::new(
629 format!("new_{name}").into(),
630 DUMMY_SP,
631 Default::default()
632 ),
633 )
634 } else {
635 quote!(
636 "(() => $expr)" as Expr,
637 expr: Expr = Expr::Member(expr),
638 )
639 }
640 })
641 }
642 EsmExport::ImportedNamespace(esm_ref) => {
643 let referenced_asset =
644 ReferencedAsset::from_resolve_result(esm_ref.resolve_reference()).await?;
645 referenced_asset
646 .get_ident(chunking_context)
647 .await?
648 .map(|ident| {
649 quote!(
650 "(() => $imported)" as Expr,
651 imported = Ident::new(ident.into(), DUMMY_SP, Default::default())
652 )
653 })
654 }
655 };
656 if let Some(expr) = expr {
657 props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
658 key: PropName::Str(Str {
659 span: DUMMY_SP,
660 value: exported.as_str().into(),
661 raw: None,
662 }),
663 value: Box::new(expr),
664 }))));
665 }
666 }
667 let getters = Expr::Object(ObjectLit {
668 span: DUMMY_SP,
669 props,
670 });
671 let dynamic_stmt = if !dynamic_exports.is_empty() {
672 Some(Stmt::Expr(ExprStmt {
673 span: DUMMY_SP,
674 expr: Expr::from_exprs(dynamic_exports),
675 }))
676 } else {
677 None
678 };
679
680 Ok(CodeGeneration::new(
681 vec![],
682 [dynamic_stmt
683 .map(|stmt| CodeGenerationHoistedStmt::new("__turbopack_dynamic__".into(), stmt))]
684 .into_iter()
685 .flatten()
686 .collect(),
687 vec![CodeGenerationHoistedStmt::new(
688 "__turbopack_esm__".into(),
689 quote!("$turbopack_esm($getters);" as Stmt,
690 turbopack_esm: Expr = TURBOPACK_ESM.into(),
691 getters: Expr = getters
692 ),
693 )],
694 ))
695 }
696}