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