1use std::{collections::BTreeMap, fmt::Display};
2
3use once_cell::sync::Lazy;
4use rustc_hash::{FxHashMap, FxHashSet};
5use swc_core::{
6 common::{BytePos, Span, Spanned, comments::Comments, source_map::SmallPos},
7 ecma::{
8 ast::*,
9 atoms::{Atom, atom},
10 utils::find_pat_ids,
11 visit::{Visit, VisitWith},
12 },
13};
14use turbo_rcstr::RcStr;
15use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc};
16use turbopack_core::{issue::IssueSource, source::Source};
17
18use super::{JsValue, ModuleValue, top_level_await::has_top_level_await};
19use crate::{
20 SpecifiedModuleType,
21 analyzer::{ConstantValue, ObjectPart},
22 tree_shake::{PartId, find_turbopack_part_id_in_asserts},
23};
24
25#[turbo_tasks::value(serialization = "auto_for_input")]
26#[derive(Default, Debug, Clone, Hash)]
27pub struct ImportAnnotations {
28 #[turbo_tasks(trace_ignore)]
30 map: BTreeMap<Atom, Atom>,
31}
32
33static ANNOTATION_TRANSITION: Lazy<Atom> =
35 Lazy::new(|| crate::annotations::ANNOTATION_TRANSITION.into());
36
37static ANNOTATION_CHUNKING_TYPE: Lazy<Atom> =
39 Lazy::new(|| crate::annotations::ANNOTATION_CHUNKING_TYPE.into());
40
41static ATTRIBUTE_MODULE_TYPE: Lazy<Atom> = Lazy::new(|| "type".into());
43
44impl ImportAnnotations {
45 pub fn parse(with: Option<&ObjectLit>) -> ImportAnnotations {
46 let Some(with) = with else {
47 return ImportAnnotations::default();
48 };
49
50 let mut map = BTreeMap::new();
51
52 for (key, value) in with.props.iter().filter_map(|prop| {
56 let kv = prop.as_prop()?.as_key_value()?;
57
58 let Lit::Str(str) = kv.value.as_lit()? else {
59 return None;
60 };
61
62 Some((&kv.key, str))
63 }) {
64 let key = match key {
65 PropName::Ident(ident) => ident.sym.as_str(),
66 PropName::Str(str) => str.value.as_str(),
67 _ => continue,
69 };
70
71 map.insert(key.into(), value.value.as_str().into());
72 }
73
74 ImportAnnotations { map }
75 }
76
77 pub fn parse_dynamic(with: &JsValue) -> Option<ImportAnnotations> {
78 let mut map = BTreeMap::new();
79
80 let JsValue::Object { parts, .. } = with else {
81 return None;
82 };
83
84 for part in parts.iter() {
85 let ObjectPart::KeyValue(key, value) = part else {
86 continue;
87 };
88 let (
89 JsValue::Constant(ConstantValue::Str(key)),
90 JsValue::Constant(ConstantValue::Str(value)),
91 ) = (key, value)
92 else {
93 continue;
94 };
95
96 map.insert(key.as_str().into(), value.as_str().into());
97 }
98
99 Some(ImportAnnotations { map })
100 }
101
102 pub fn transition(&self) -> Option<&str> {
104 self.get(&ANNOTATION_TRANSITION)
105 }
106
107 pub fn chunking_type(&self) -> Option<&str> {
109 self.get(&ANNOTATION_CHUNKING_TYPE)
110 }
111
112 pub fn module_type(&self) -> Option<&str> {
114 self.get(&ATTRIBUTE_MODULE_TYPE)
115 }
116
117 pub fn get(&self, key: &Atom) -> Option<&str> {
118 self.map.get(key).map(|w| w.as_str())
119 }
120}
121
122impl Display for ImportAnnotations {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 let mut it = self.map.iter();
125 if let Some((k, v)) = it.next() {
126 write!(f, "{{ {k}: {v}")?
127 } else {
128 return f.write_str("{}");
129 };
130 for (k, v) in it {
131 write!(f, ", {k}: {v}")?
132 }
133 f.write_str(" }")
134 }
135}
136
137#[derive(Debug)]
138pub(crate) enum Reexport {
139 Star,
140 Namespace { exported: Atom },
141 Named { imported: Atom, exported: Atom },
142}
143
144#[derive(Default, Debug)]
149pub(crate) struct ImportMap {
150 imports: FxIndexMap<Id, (usize, Atom)>,
152
153 namespace_imports: FxIndexMap<Id, usize>,
155
156 reexports: Vec<(usize, Reexport)>,
158
159 references: FxIndexSet<ImportMapReference>,
161
162 has_imports: bool,
164
165 has_exports: bool,
167
168 has_top_level_await: bool,
170
171 attributes: FxHashMap<BytePos, ImportAttributes>,
178
179 full_star_imports: FxHashSet<Atom>,
182
183 pub(crate) exports: FxHashMap<RcStr, Id>,
184}
185
186#[derive(Debug)]
191pub struct ImportAttributes {
192 pub ignore: bool,
203}
204
205impl ImportAttributes {
206 pub const fn empty() -> Self {
207 ImportAttributes { ignore: false }
208 }
209
210 pub fn empty_ref() -> &'static Self {
211 static DEFAULT_VALUE: ImportAttributes = ImportAttributes::empty();
213 &DEFAULT_VALUE
214 }
215}
216
217impl Default for ImportAttributes {
218 fn default() -> Self {
219 ImportAttributes::empty()
220 }
221}
222
223impl Default for &ImportAttributes {
224 fn default() -> Self {
225 ImportAttributes::empty_ref()
226 }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Hash)]
230pub(crate) enum ImportedSymbol {
231 ModuleEvaluation,
232 Symbol(Atom),
233 Exports,
234 Part(u32),
235 PartEvaluation(u32),
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Hash)]
239pub(crate) struct ImportMapReference {
240 pub module_path: Atom,
241 pub imported_symbol: ImportedSymbol,
242 pub annotations: ImportAnnotations,
243 pub issue_source: Option<IssueSource>,
244}
245
246impl ImportMap {
247 pub fn is_esm(&self, specified_type: SpecifiedModuleType) -> bool {
248 if self.has_exports {
249 return true;
250 }
251
252 match specified_type {
253 SpecifiedModuleType::Automatic => {
254 self.has_exports || self.has_imports || self.has_top_level_await
255 }
256 SpecifiedModuleType::CommonJs => false,
257 SpecifiedModuleType::EcmaScript => true,
258 }
259 }
260
261 pub fn get_import(&self, id: &Id) -> Option<JsValue> {
262 if let Some((i, i_sym)) = self.imports.get(id) {
263 let r = &self.references[*i];
264 return Some(JsValue::member(
265 Box::new(JsValue::Module(ModuleValue {
266 module: r.module_path.clone(),
267 annotations: r.annotations.clone(),
268 })),
269 Box::new(i_sym.clone().into()),
270 ));
271 }
272 if let Some(i) = self.namespace_imports.get(id) {
273 let r = &self.references[*i];
274 return Some(JsValue::Module(ModuleValue {
275 module: r.module_path.clone(),
276 annotations: r.annotations.clone(),
277 }));
278 }
279 None
280 }
281
282 pub fn get_attributes(&self, span: Span) -> &ImportAttributes {
283 self.attributes.get(&span.lo).unwrap_or_default()
284 }
285
286 pub fn get_binding(&self, id: &Id) -> Option<(usize, Option<RcStr>)> {
288 if let Some((i, i_sym)) = self.imports.get(id) {
289 return Some((*i, Some(i_sym.as_str().into())));
290 }
291 if let Some(i) = self.namespace_imports.get(id) {
292 return Some((*i, None));
293 }
294 None
295 }
296
297 pub fn references(&self) -> impl ExactSizeIterator<Item = &ImportMapReference> {
298 self.references.iter()
299 }
300
301 pub fn reexports(&self) -> impl ExactSizeIterator<Item = (usize, &Reexport)> {
302 self.reexports.iter().map(|(i, r)| (*i, r))
303 }
304
305 pub(super) fn analyze(
307 m: &Program,
308 source: Option<ResolvedVc<Box<dyn Source>>>,
309 comments: Option<&dyn Comments>,
310 ) -> Self {
311 let mut data = ImportMap::default();
312
313 if let Program::Module(m) = m {
319 let mut candidates = FxIndexMap::default();
320
321 m.body.iter().for_each(|stmt| {
324 if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = stmt {
325 for s in &import.specifiers {
326 if let ImportSpecifier::Namespace(s) = s {
327 candidates.insert(s.local.to_id(), import.src.value.clone());
328 }
329 }
330 }
331 });
332
333 let mut analyzer = StarImportAnalyzer {
334 candidates,
335 full_star_imports: &mut data.full_star_imports,
336 };
337 m.visit_with(&mut analyzer);
338 }
339
340 let mut analyzer = Analyzer {
341 data: &mut data,
342 source,
343 comments,
344 };
345 m.visit_with(&mut analyzer);
346
347 data
348 }
349
350 pub(crate) fn should_import_all(&self, esm_reference_index: usize) -> bool {
351 let r = &self.references[esm_reference_index];
352
353 self.full_star_imports.contains(&r.module_path)
354 }
355}
356
357struct StarImportAnalyzer<'a> {
358 candidates: FxIndexMap<Id, Atom>,
360 full_star_imports: &'a mut FxHashSet<Atom>,
361}
362
363impl Visit for StarImportAnalyzer<'_> {
364 fn visit_expr(&mut self, node: &Expr) {
365 if let Expr::Ident(i) = node {
366 if let Some(module_path) = self.candidates.get(&i.to_id()) {
367 self.full_star_imports.insert(module_path.clone());
368 return;
369 }
370 }
371
372 node.visit_children_with(self);
373 }
374
375 fn visit_import_decl(&mut self, _: &ImportDecl) {}
376
377 fn visit_member_expr(&mut self, node: &MemberExpr) {
378 match &node.prop {
379 MemberProp::Ident(..) | MemberProp::PrivateName(..) => {
380 if node.obj.is_ident() {
381 return;
382 }
383 node.obj.visit_children_with(self);
385 }
386 MemberProp::Computed(..) => {
387 node.obj.visit_with(self);
388 node.prop.visit_with(self);
389 }
390 }
391 }
392
393 fn visit_pat(&mut self, pat: &Pat) {
394 if let Pat::Ident(i) = pat {
395 if let Some(module_path) = self.candidates.get(&i.to_id()) {
396 self.full_star_imports.insert(module_path.clone());
397 return;
398 }
399 }
400
401 pat.visit_children_with(self);
402 }
403
404 fn visit_simple_assign_target(&mut self, node: &SimpleAssignTarget) {
405 if let SimpleAssignTarget::Ident(i) = node {
406 if let Some(module_path) = self.candidates.get(&i.to_id()) {
407 self.full_star_imports.insert(module_path.clone());
408 return;
409 }
410 }
411
412 node.visit_children_with(self);
413 }
414}
415
416struct Analyzer<'a> {
417 data: &'a mut ImportMap,
418 source: Option<ResolvedVc<Box<dyn Source>>>,
419 comments: Option<&'a dyn Comments>,
420}
421
422impl Analyzer<'_> {
423 fn ensure_reference(
424 &mut self,
425 span: Span,
426 module_path: Atom,
427 imported_symbol: ImportedSymbol,
428 annotations: ImportAnnotations,
429 ) -> Option<usize> {
430 let issue_source = self
431 .source
432 .map(|s| IssueSource::from_swc_offsets(s, span.lo.to_u32(), span.hi.to_u32()));
433
434 let r = ImportMapReference {
435 module_path,
436 imported_symbol,
437 issue_source,
438 annotations,
439 };
440 if let Some(i) = self.data.references.get_index_of(&r) {
441 Some(i)
442 } else {
443 let i = self.data.references.len();
444 self.data.references.insert(r);
445 Some(i)
446 }
447 }
448}
449
450fn to_word(name: &ModuleExportName) -> Atom {
451 match name {
452 ModuleExportName::Ident(ident) => ident.sym.clone(),
453 ModuleExportName::Str(str) => str.value.clone(),
454 }
455}
456
457impl Visit for Analyzer<'_> {
458 fn visit_import_decl(&mut self, import: &ImportDecl) {
459 self.data.has_imports = true;
460
461 let annotations = ImportAnnotations::parse(import.with.as_deref());
462
463 let internal_symbol = parse_with(import.with.as_deref());
464
465 if internal_symbol.is_none() {
466 self.ensure_reference(
467 import.span,
468 import.src.value.clone(),
469 ImportedSymbol::ModuleEvaluation,
470 annotations.clone(),
471 );
472 }
473
474 for s in &import.specifiers {
475 let symbol = internal_symbol
476 .clone()
477 .unwrap_or_else(|| get_import_symbol_from_import(s));
478 let i = self.ensure_reference(
479 import.span,
480 import.src.value.clone(),
481 symbol,
482 annotations.clone(),
483 );
484 let i = match i {
485 Some(v) => v,
486 None => continue,
487 };
488
489 let (local, orig_sym) = match s {
490 ImportSpecifier::Named(ImportNamedSpecifier {
491 local, imported, ..
492 }) => match imported {
493 Some(imported) => (local.to_id(), orig_name(imported)),
494 _ => (local.to_id(), local.sym.clone()),
495 },
496 ImportSpecifier::Default(s) => (s.local.to_id(), "default".into()),
497 ImportSpecifier::Namespace(s) => {
498 self.data.namespace_imports.insert(s.local.to_id(), i);
499 continue;
500 }
501 };
502
503 self.data.imports.insert(local, (i, orig_sym));
504 }
505 if import.specifiers.is_empty() {
506 if let Some(internal_symbol) = internal_symbol {
507 self.ensure_reference(
508 import.span,
509 import.src.value.clone(),
510 internal_symbol,
511 annotations,
512 );
513 }
514 }
515 }
516
517 fn visit_export_all(&mut self, export: &ExportAll) {
518 self.data.has_exports = true;
519
520 let annotations = ImportAnnotations::parse(export.with.as_deref());
521
522 self.ensure_reference(
523 export.span,
524 export.src.value.clone(),
525 ImportedSymbol::ModuleEvaluation,
526 annotations.clone(),
527 );
528 let symbol = parse_with(export.with.as_deref());
529
530 let i = self.ensure_reference(
531 export.span,
532 export.src.value.clone(),
533 symbol.unwrap_or(ImportedSymbol::Exports),
534 annotations,
535 );
536 if let Some(i) = i {
537 self.data.reexports.push((i, Reexport::Star));
538 }
539 }
540
541 fn visit_named_export(&mut self, export: &NamedExport) {
542 self.data.has_exports = true;
543
544 let Some(ref src) = export.src else {
545 return;
546 };
547
548 let annotations = ImportAnnotations::parse(export.with.as_deref());
549
550 let internal_symbol = parse_with(export.with.as_deref());
551
552 if internal_symbol.is_none() || export.specifiers.is_empty() {
553 self.ensure_reference(
554 export.span,
555 src.value.clone(),
556 ImportedSymbol::ModuleEvaluation,
557 annotations.clone(),
558 );
559 }
560
561 for spec in export.specifiers.iter() {
562 let symbol = internal_symbol
563 .clone()
564 .unwrap_or_else(|| get_import_symbol_from_export(spec));
565
566 let i =
567 self.ensure_reference(export.span, src.value.clone(), symbol, annotations.clone());
568 let i = match i {
569 Some(v) => v,
570 None => continue,
571 };
572
573 match spec {
574 ExportSpecifier::Namespace(n) => {
575 self.data.reexports.push((
576 i,
577 Reexport::Namespace {
578 exported: to_word(&n.name),
579 },
580 ));
581 }
582 ExportSpecifier::Default(d) => {
583 self.data.reexports.push((
584 i,
585 Reexport::Named {
586 imported: atom!("default"),
587 exported: d.exported.sym.clone(),
588 },
589 ));
590 }
591 ExportSpecifier::Named(n) => {
592 self.data.reexports.push((
593 i,
594 Reexport::Named {
595 imported: to_word(&n.orig),
596 exported: to_word(n.exported.as_ref().unwrap_or(&n.orig)),
597 },
598 ));
599 }
600 }
601 }
602 }
603
604 fn visit_export_decl(&mut self, n: &ExportDecl) {
605 self.data.has_exports = true;
606
607 if self.comments.is_some() {
608 n.visit_children_with(self);
610 }
611
612 match &n.decl {
613 Decl::Class(n) => {
614 self.data
615 .exports
616 .insert(n.ident.sym.as_str().into(), n.ident.to_id());
617 }
618 Decl::Fn(n) => {
619 self.data
620 .exports
621 .insert(n.ident.sym.as_str().into(), n.ident.to_id());
622 }
623 Decl::Var(..) | Decl::Using(..) => {
624 let ids: Vec<Id> = find_pat_ids(&n.decl);
625 for id in ids {
626 self.data.exports.insert(id.0.as_str().into(), id);
627 }
628 }
629 _ => {}
630 }
631 }
632
633 fn visit_export_default_decl(&mut self, n: &ExportDefaultDecl) {
634 self.data.has_exports = true;
635
636 if self.comments.is_some() {
637 n.visit_children_with(self);
639 }
640 }
641 fn visit_export_default_expr(&mut self, n: &ExportDefaultExpr) {
642 self.data.has_exports = true;
643
644 if self.comments.is_some() {
645 n.visit_children_with(self);
647 }
648 }
649
650 fn visit_export_named_specifier(&mut self, n: &ExportNamedSpecifier) {
651 if let ModuleExportName::Ident(ident) = &n.exported.as_ref().unwrap_or(&n.orig) {
652 self.data
653 .exports
654 .insert(ident.sym.as_str().into(), ident.to_id());
655 }
656 }
657
658 fn visit_export_default_specifier(&mut self, n: &ExportDefaultSpecifier) {
659 self.data
660 .exports
661 .insert("default".into(), n.exported.to_id());
662 }
663
664 fn visit_export_namespace_specifier(&mut self, n: &ExportNamespaceSpecifier) {
665 if let ModuleExportName::Ident(ident) = &n.name {
666 self.data
667 .exports
668 .insert(ident.sym.as_str().into(), ident.to_id());
669 }
670 }
671
672 fn visit_program(&mut self, m: &Program) {
673 self.data.has_top_level_await = has_top_level_await(m).is_some();
674
675 m.visit_children_with(self);
676 }
677
678 fn visit_stmt(&mut self, n: &Stmt) {
679 if self.comments.is_some() {
680 n.visit_children_with(self);
682 }
683 }
684
685 fn visit_call_expr(&mut self, n: &CallExpr) {
697 if let Some(comments) = self.comments {
699 let callee_span = match &n.callee {
700 Callee::Import(Import { span, .. }) => Some(*span),
701 Callee::Expr(e) => Some(e.span()),
702 _ => None,
703 };
704
705 let ignore_directive = parse_ignore_directive(comments, n.args.first());
706
707 if let Some((callee_span, ignore_directive)) = callee_span.zip(ignore_directive) {
708 self.data.attributes.insert(
709 callee_span.lo,
710 ImportAttributes {
711 ignore: ignore_directive,
712 },
713 );
714 };
715 }
716
717 n.visit_children_with(self);
718 }
719
720 fn visit_new_expr(&mut self, n: &NewExpr) {
721 if let Some(comments) = self.comments {
723 let callee_span = match &n.callee {
724 box Expr::Ident(Ident { sym, .. }) if sym == "Worker" => Some(n.span),
725 _ => None,
726 };
727
728 let ignore_directive = parse_ignore_directive(comments, n.args.iter().flatten().next());
729
730 if let Some((callee_span, ignore_directive)) = callee_span.zip(ignore_directive) {
731 self.data.attributes.insert(
732 callee_span.lo,
733 ImportAttributes {
734 ignore: ignore_directive,
735 },
736 );
737 };
738 }
739
740 n.visit_children_with(self);
741 }
742}
743
744fn parse_ignore_directive(comments: &dyn Comments, value: Option<&ExprOrSpread>) -> Option<bool> {
745 value
747 .map(|arg| arg.span_lo())
748 .and_then(|comment_pos| comments.get_leading(comment_pos))
749 .iter()
750 .flatten()
751 .rev()
752 .filter_map(|comment| {
753 let (directive, value) = comment.text.trim().split_once(':')?;
754 match (directive.trim(), value.trim()) {
756 ("webpackIgnore" | "turbopackIgnore", "true") => Some(true),
757 ("webpackIgnore" | "turbopackIgnore", "false") => Some(false),
758 _ => None, }
760 })
761 .next()
762}
763
764pub(crate) fn orig_name(n: &ModuleExportName) -> Atom {
765 match n {
766 ModuleExportName::Ident(v) => v.sym.clone(),
767 ModuleExportName::Str(v) => v.value.clone(),
768 }
769}
770
771fn parse_with(with: Option<&ObjectLit>) -> Option<ImportedSymbol> {
772 find_turbopack_part_id_in_asserts(with?).map(|v| match v {
773 PartId::Internal(index, true) => ImportedSymbol::PartEvaluation(index),
774 PartId::Internal(index, false) => ImportedSymbol::Part(index),
775 PartId::ModuleEvaluation => ImportedSymbol::ModuleEvaluation,
776 PartId::Export(e) => ImportedSymbol::Symbol(e.as_str().into()),
777 PartId::Exports => ImportedSymbol::Exports,
778 })
779}
780
781fn get_import_symbol_from_import(specifier: &ImportSpecifier) -> ImportedSymbol {
782 match specifier {
783 ImportSpecifier::Named(ImportNamedSpecifier {
784 local, imported, ..
785 }) => ImportedSymbol::Symbol(match imported {
786 Some(imported) => orig_name(imported),
787 _ => local.sym.clone(),
788 }),
789 ImportSpecifier::Default(..) => ImportedSymbol::Symbol(atom!("default")),
790 ImportSpecifier::Namespace(..) => ImportedSymbol::Exports,
791 }
792}
793
794fn get_import_symbol_from_export(specifier: &ExportSpecifier) -> ImportedSymbol {
795 match specifier {
796 ExportSpecifier::Named(ExportNamedSpecifier { orig, .. }) => {
797 ImportedSymbol::Symbol(orig_name(orig))
798 }
799 ExportSpecifier::Default(..) => ImportedSymbol::Symbol(atom!("default")),
800 ExportSpecifier::Namespace(..) => ImportedSymbol::Exports,
801 }
802}