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