1use std::{borrow::Cow, collections::BTreeMap, fmt::Display, sync::Arc};
2
3use once_cell::sync::Lazy;
4use rustc_hash::{FxHashMap, FxHashSet};
5use smallvec::SmallVec;
6use swc_core::{
7 atoms::Wtf8Atom,
8 common::{BytePos, Span, Spanned, SyntaxContext, comments::Comments, source_map::SmallPos},
9 ecma::{
10 ast::*,
11 atoms::{Atom, atom},
12 utils::{IsDirective, find_pat_ids},
13 visit::{Visit, VisitWith},
14 },
15};
16use turbo_rcstr::{RcStr, rcstr};
17use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc};
18use turbopack_core::{issue::IssueSource, loader::WebpackLoaderItem, source::Source};
19
20use super::{JsValue, ModuleValue, top_level_await::has_top_level_await};
21use crate::{
22 SpecifiedModuleType,
23 analyzer::{ConstantValue, ObjectPart},
24 magic_identifier,
25 references::util::{SpecifiedChunkingType, parse_chunking_type_annotation},
26 tree_shake::{PartId, find_turbopack_part_id_in_asserts},
27};
28
29#[turbo_tasks::value]
30#[derive(Default, Debug, Clone, Hash)]
31pub struct ImportAnnotations {
32 #[turbo_tasks(trace_ignore)]
34 #[bincode(with_serde)]
35 map: BTreeMap<Wtf8Atom, Wtf8Atom>,
36 #[turbo_tasks(trace_ignore)]
39 #[bincode(with_serde)]
40 turbopack_loader: Option<WebpackLoaderItem>,
41 turbopack_rename_as: Option<RcStr>,
42 turbopack_module_type: Option<RcStr>,
43 chunking_type: Option<SpecifiedChunkingType>,
44}
45
46static ANNOTATION_TRANSITION: Lazy<Wtf8Atom> =
48 Lazy::new(|| crate::annotations::ANNOTATION_TRANSITION.into());
49
50static ATTRIBUTE_MODULE_TYPE: Lazy<Wtf8Atom> = Lazy::new(|| atom!("type").into());
52
53impl ImportAnnotations {
54 pub fn parse(with: Option<&ObjectLit>) -> Option<ImportAnnotations> {
55 let with = with?;
56
57 let mut map = BTreeMap::new();
58 let mut turbopack_loader_name: Option<RcStr> = None;
59 let mut turbopack_loader_options: serde_json::Map<String, serde_json::Value> =
60 serde_json::Map::new();
61 let mut turbopack_rename_as: Option<RcStr> = None;
62 let mut turbopack_module_type: Option<RcStr> = None;
63 let mut chunking_type: Option<SpecifiedChunkingType> = None;
64
65 for prop in &with.props {
66 let Some(kv) = prop.as_prop().and_then(|p| p.as_key_value()) else {
67 continue;
68 };
69
70 let key_str = match &kv.key {
71 PropName::Ident(ident) => Cow::Borrowed(ident.sym.as_str()),
72 PropName::Str(str) => str.value.to_string_lossy(),
73 _ => continue,
74 };
75
76 match &*key_str {
78 "turbopackLoader" => {
79 if let Some(Lit::Str(s)) = kv.value.as_lit() {
80 turbopack_loader_name =
81 Some(RcStr::from(s.value.to_string_lossy().into_owned()));
82 }
83 }
84 "turbopackLoaderOptions" => {
85 if let Some(Lit::Str(s)) = kv.value.as_lit() {
86 let json_str = s.value.to_string_lossy();
87 if let Ok(serde_json::Value::Object(map)) = serde_json::from_str(&json_str)
88 {
89 turbopack_loader_options = map;
90 }
91 }
92 }
93 "turbopackAs" => {
94 if let Some(Lit::Str(s)) = kv.value.as_lit() {
95 turbopack_rename_as =
96 Some(RcStr::from(s.value.to_string_lossy().into_owned()));
97 }
98 }
99 "turbopackModuleType" => {
100 if let Some(Lit::Str(s)) = kv.value.as_lit() {
101 turbopack_module_type =
102 Some(RcStr::from(s.value.to_string_lossy().into_owned()));
103 }
104 }
105 "turbopack-chunking-type" => {
106 if let Some(Lit::Str(s)) = kv.value.as_lit() {
107 chunking_type = parse_chunking_type_annotation(
108 kv.value.span(),
109 &s.value.to_string_lossy(),
110 );
111 }
112 }
113 _ => {
114 if let Some(Lit::Str(str)) = kv.value.as_lit() {
116 let key: Wtf8Atom = match &kv.key {
117 PropName::Ident(ident) => ident.sym.clone().into(),
118 PropName::Str(s) => s.value.clone(),
119 _ => continue,
120 };
121 map.insert(key, str.value.clone());
122 }
123 }
124 }
125 }
126
127 let turbopack_loader = turbopack_loader_name.map(|name| WebpackLoaderItem {
128 loader: name,
129 options: turbopack_loader_options,
130 });
131
132 if !map.is_empty()
133 || turbopack_loader.is_some()
134 || turbopack_rename_as.is_some()
135 || turbopack_module_type.is_some()
136 || chunking_type.is_some()
137 {
138 Some(ImportAnnotations {
139 map,
140 turbopack_loader,
141 turbopack_rename_as,
142 turbopack_module_type,
143 chunking_type,
144 })
145 } else {
146 None
147 }
148 }
149
150 pub fn parse_dynamic(with: &JsValue) -> Option<ImportAnnotations> {
151 let mut map = BTreeMap::new();
152
153 let JsValue::Object { parts, .. } = with else {
154 return None;
155 };
156
157 for part in parts.iter() {
158 let ObjectPart::KeyValue(key, value) = part else {
159 continue;
160 };
161 let (
162 JsValue::Constant(ConstantValue::Str(key)),
163 JsValue::Constant(ConstantValue::Str(value)),
164 ) = (key, value)
165 else {
166 continue;
167 };
168
169 map.insert(
170 key.as_atom().into_owned().into(),
171 value.as_atom().into_owned().into(),
172 );
173 }
174
175 if !map.is_empty() {
176 Some(ImportAnnotations {
177 map,
178 turbopack_loader: None,
179 turbopack_rename_as: None,
180 turbopack_module_type: None,
181 chunking_type: None,
182 })
183 } else {
184 None
185 }
186 }
187
188 pub fn transition(&self) -> Option<Cow<'_, str>> {
190 self.get(&ANNOTATION_TRANSITION)
191 .map(|v| v.to_string_lossy())
192 }
193
194 pub fn chunking_type(&self) -> Option<SpecifiedChunkingType> {
196 self.chunking_type
197 }
198
199 pub fn module_type(&self) -> Option<&Wtf8Atom> {
201 self.get(&ATTRIBUTE_MODULE_TYPE)
202 }
203
204 pub fn turbopack_loader(&self) -> Option<&WebpackLoaderItem> {
206 self.turbopack_loader.as_ref()
207 }
208
209 pub fn turbopack_rename_as(&self) -> Option<&RcStr> {
211 self.turbopack_rename_as.as_ref()
212 }
213
214 pub fn turbopack_module_type(&self) -> Option<&RcStr> {
216 self.turbopack_module_type.as_ref()
217 }
218
219 pub fn has_turbopack_loader(&self) -> bool {
221 self.turbopack_loader.is_some()
222 }
223
224 pub fn get(&self, key: &Wtf8Atom) -> Option<&Wtf8Atom> {
225 self.map.get(key)
226 }
227}
228
229impl Display for ImportAnnotations {
230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231 let mut it = self.map.iter();
232 if let Some((k, v)) = it.next() {
233 write!(f, "{{ {}: {}", k.to_string_lossy(), v.to_string_lossy())?
234 } else {
235 return f.write_str("{}");
236 };
237 for (k, v) in it {
238 write!(f, ", {}: {}", k.to_string_lossy(), v.to_string_lossy())?
239 }
240 f.write_str(" }")
241 }
242}
243
244#[derive(Debug)]
245pub(crate) enum Reexport {
246 Star,
247 Namespace { exported: Atom },
248 Named { imported: Atom, exported: Atom },
249}
250
251#[derive(Default, Debug)]
256pub(crate) struct ImportMap {
257 imports: FxIndexMap<Id, (usize, Atom)>,
259
260 namespace_imports: FxIndexMap<Id, usize>,
262
263 reexports: Vec<(usize, Reexport)>,
265
266 references: FxIndexSet<ImportMapReference>,
268
269 has_imports: bool,
271
272 has_exports: bool,
274
275 has_top_level_await: bool,
277
278 pub(crate) strict: bool,
280
281 attributes: FxHashMap<BytePos, ImportAttributes>,
288
289 full_star_imports: FxHashSet<Wtf8Atom>,
292
293 pub(crate) exports: FxHashMap<RcStr, Id>,
294}
295
296#[derive(Debug)]
301pub struct ImportAttributes {
302 pub ignore: bool,
313 pub optional: bool,
323 pub export_names: Option<SmallVec<[RcStr; 1]>>,
337 pub chunking_type: Option<SpecifiedChunkingType>,
346}
347
348impl ImportAttributes {
349 pub const fn empty() -> Self {
350 ImportAttributes {
351 ignore: false,
352 optional: false,
353 export_names: None,
354 chunking_type: None,
355 }
356 }
357
358 pub fn empty_ref() -> &'static Self {
359 static DEFAULT_VALUE: ImportAttributes = ImportAttributes::empty();
361 &DEFAULT_VALUE
362 }
363}
364
365impl Default for ImportAttributes {
366 fn default() -> Self {
367 ImportAttributes::empty()
368 }
369}
370
371impl Default for &ImportAttributes {
372 fn default() -> Self {
373 ImportAttributes::empty_ref()
374 }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq, Hash)]
378pub(crate) enum ImportedSymbol {
379 ModuleEvaluation,
380 Symbol(Atom),
381 Exports,
382 Part(u32),
383 PartEvaluation(u32),
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Hash)]
387pub(crate) struct ImportMapReference {
388 pub module_path: Wtf8Atom,
389 pub imported_symbol: ImportedSymbol,
390 pub annotations: Option<Arc<ImportAnnotations>>,
391 pub issue_source: Option<IssueSource>,
392}
393
394impl ImportMap {
395 pub fn is_esm(&self, specified_type: SpecifiedModuleType) -> bool {
396 if self.has_exports {
397 return true;
398 }
399
400 match specified_type {
401 SpecifiedModuleType::Automatic => {
402 self.has_exports || self.has_imports || self.has_top_level_await
403 }
404 SpecifiedModuleType::CommonJs => false,
405 SpecifiedModuleType::EcmaScript => true,
406 }
407 }
408
409 pub fn get_import(&self, id: &Id) -> Option<JsValue> {
410 if let Some((i, i_sym)) = self.imports.get(id) {
411 let r = &self.references[*i];
412 return Some(JsValue::member(
413 Box::new(JsValue::Module(ModuleValue {
414 module: r.module_path.clone(),
415 annotations: r.annotations.clone(),
416 })),
417 Box::new(i_sym.clone().into()),
418 ));
419 }
420 if let Some(i) = self.namespace_imports.get(id) {
421 let r = &self.references[*i];
422 return Some(JsValue::Module(ModuleValue {
423 module: r.module_path.clone(),
424 annotations: r.annotations.clone(),
425 }));
426 }
427 None
428 }
429
430 pub fn get_attributes(&self, span: Span) -> &ImportAttributes {
431 self.attributes.get(&span.lo).unwrap_or_default()
432 }
433
434 pub fn get_binding(&self, id: &Id) -> Option<(usize, Option<RcStr>)> {
436 if let Some((i, i_sym)) = self.imports.get(id) {
437 return Some((*i, Some(i_sym.as_str().into())));
438 }
439 if let Some(i) = self.namespace_imports.get(id) {
440 return Some((*i, None));
441 }
442 None
443 }
444
445 pub fn references(&self) -> impl ExactSizeIterator<Item = &ImportMapReference> {
446 self.references.iter()
447 }
448
449 pub fn reexports(&self) -> impl ExactSizeIterator<Item = (usize, &Reexport)> {
450 self.reexports.iter().map(|(i, r)| (*i, r))
451 }
452
453 pub(super) fn analyze(
455 m: &Program,
456 source: Option<ResolvedVc<Box<dyn Source>>>,
457 comments: Option<&dyn Comments>,
458 ) -> Self {
459 let mut data = ImportMap::default();
460
461 if let Program::Module(m) = m {
467 let mut candidates = FxIndexMap::default();
468
469 m.body.iter().for_each(|stmt| {
472 if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = stmt {
473 for s in &import.specifiers {
474 if let ImportSpecifier::Namespace(s) = s {
475 candidates.insert(s.local.to_id(), import.src.value.clone());
476 }
477 }
478 }
479 });
480
481 let mut analyzer = StarImportAnalyzer {
482 candidates,
483 full_star_imports: &mut data.full_star_imports,
484 };
485 m.visit_with(&mut analyzer);
486 }
487
488 let mut analyzer = Analyzer {
489 data: &mut data,
490 source,
491 comments,
492 };
493 m.visit_with(&mut analyzer);
494
495 data
496 }
497
498 pub(crate) fn should_import_all(&self, esm_reference_index: usize) -> bool {
499 let r = &self.references[esm_reference_index];
500
501 self.full_star_imports.contains(&r.module_path)
502 }
503}
504
505struct StarImportAnalyzer<'a> {
506 candidates: FxIndexMap<Id, Wtf8Atom>,
508 full_star_imports: &'a mut FxHashSet<Wtf8Atom>,
509}
510
511impl Visit for StarImportAnalyzer<'_> {
512 fn visit_expr(&mut self, node: &Expr) {
513 if let Expr::Ident(i) = node
514 && let Some(module_path) = self.candidates.get(&i.to_id())
515 {
516 self.full_star_imports.insert(module_path.clone());
517 return;
518 }
519
520 node.visit_children_with(self);
521 }
522
523 fn visit_import_decl(&mut self, _: &ImportDecl) {}
524
525 fn visit_member_expr(&mut self, node: &MemberExpr) {
526 match &node.prop {
527 MemberProp::Ident(..) | MemberProp::PrivateName(..) => {
528 if node.obj.is_ident() {
529 return;
530 }
531 node.obj.visit_children_with(self);
533 }
534 MemberProp::Computed(..) => {
535 node.obj.visit_with(self);
536 node.prop.visit_with(self);
537 }
538 }
539 }
540
541 fn visit_pat(&mut self, pat: &Pat) {
542 if let Pat::Ident(i) = pat
543 && let Some(module_path) = self.candidates.get(&i.to_id())
544 {
545 self.full_star_imports.insert(module_path.clone());
546 return;
547 }
548
549 pat.visit_children_with(self);
550 }
551
552 fn visit_simple_assign_target(&mut self, node: &SimpleAssignTarget) {
553 if let SimpleAssignTarget::Ident(i) = node
554 && let Some(module_path) = self.candidates.get(&i.to_id())
555 {
556 self.full_star_imports.insert(module_path.clone());
557 return;
558 }
559
560 node.visit_children_with(self);
561 }
562}
563
564struct Analyzer<'a> {
565 data: &'a mut ImportMap,
566 source: Option<ResolvedVc<Box<dyn Source>>>,
567 comments: Option<&'a dyn Comments>,
568}
569
570impl Analyzer<'_> {
571 fn ensure_reference(
572 &mut self,
573 span: Span,
574 module_path: Wtf8Atom,
575 imported_symbol: ImportedSymbol,
576 annotations: Option<ImportAnnotations>,
577 ) -> Option<usize> {
578 let issue_source = self
579 .source
580 .map(|s| IssueSource::from_swc_offsets(s, span.lo.to_u32(), span.hi.to_u32()));
581
582 let r = ImportMapReference {
583 module_path,
584 imported_symbol,
585 issue_source,
586 annotations: annotations.map(Arc::new),
587 };
588 if let Some(i) = self.data.references.get_index_of(&r) {
589 Some(i)
590 } else {
591 let i = self.data.references.len();
592 self.data.references.insert(r);
593 Some(i)
594 }
595 }
596}
597
598impl Visit for Analyzer<'_> {
599 fn visit_import_decl(&mut self, import: &ImportDecl) {
600 self.data.has_imports = true;
601
602 let annotations = ImportAnnotations::parse(import.with.as_deref());
603
604 let internal_symbol = parse_with(import.with.as_deref());
605
606 if internal_symbol.is_none() {
607 self.ensure_reference(
608 import.span,
609 import.src.value.clone(),
610 ImportedSymbol::ModuleEvaluation,
611 annotations.clone(),
612 );
613 }
614
615 for s in &import.specifiers {
616 let symbol = internal_symbol
617 .clone()
618 .unwrap_or_else(|| get_import_symbol_from_import(s));
619 let i = self.ensure_reference(
620 import.span,
621 import.src.value.clone(),
622 symbol,
623 annotations.clone(),
624 );
625 let i = match i {
626 Some(v) => v,
627 None => continue,
628 };
629
630 let (local, orig_sym) = match s {
631 ImportSpecifier::Named(ImportNamedSpecifier {
632 local, imported, ..
633 }) => match imported {
634 Some(imported) => (local.to_id(), imported.atom().into_owned()),
635 _ => (local.to_id(), local.sym.clone()),
636 },
637 ImportSpecifier::Default(s) => (s.local.to_id(), atom!("default")),
638 ImportSpecifier::Namespace(s) => {
639 self.data.namespace_imports.insert(s.local.to_id(), i);
640 continue;
641 }
642 };
643
644 self.data.imports.insert(local, (i, orig_sym));
645 }
646 if import.specifiers.is_empty()
647 && let Some(internal_symbol) = internal_symbol
648 {
649 self.ensure_reference(
650 import.span,
651 import.src.value.clone(),
652 internal_symbol,
653 annotations,
654 );
655 }
656 }
657
658 fn visit_export_all(&mut self, export: &ExportAll) {
659 self.data.has_exports = true;
660
661 let annotations = ImportAnnotations::parse(export.with.as_deref());
662
663 self.ensure_reference(
664 export.span,
665 export.src.value.clone(),
666 ImportedSymbol::ModuleEvaluation,
667 annotations.clone(),
668 );
669 let symbol = parse_with(export.with.as_deref());
670
671 let i = self.ensure_reference(
672 export.span,
673 export.src.value.clone(),
674 symbol.unwrap_or(ImportedSymbol::Exports),
675 annotations,
676 );
677 if let Some(i) = i {
678 self.data.reexports.push((i, Reexport::Star));
679 }
680 }
681
682 fn visit_named_export(&mut self, export: &NamedExport) {
683 self.data.has_exports = true;
684
685 let Some(ref src) = export.src else {
686 export.visit_children_with(self);
687 return;
688 };
689
690 let annotations = ImportAnnotations::parse(export.with.as_deref());
691
692 let internal_symbol = parse_with(export.with.as_deref());
693
694 if internal_symbol.is_none() || export.specifiers.is_empty() {
695 self.ensure_reference(
696 export.span,
697 src.value.clone(),
698 ImportedSymbol::ModuleEvaluation,
699 annotations.clone(),
700 );
701 }
702
703 for spec in export.specifiers.iter() {
704 let symbol = internal_symbol
705 .clone()
706 .unwrap_or_else(|| get_import_symbol_from_export(spec));
707
708 let i =
709 self.ensure_reference(export.span, src.value.clone(), symbol, annotations.clone());
710 let i = match i {
711 Some(v) => v,
712 None => continue,
713 };
714
715 match spec {
716 ExportSpecifier::Namespace(n) => {
717 self.data.reexports.push((
718 i,
719 Reexport::Namespace {
720 exported: n.name.atom().into_owned(),
721 },
722 ));
723 }
724 ExportSpecifier::Default(d) => {
725 self.data.reexports.push((
726 i,
727 Reexport::Named {
728 imported: atom!("default"),
729 exported: d.exported.sym.clone(),
730 },
731 ));
732 }
733 ExportSpecifier::Named(n) => {
734 self.data.reexports.push((
735 i,
736 Reexport::Named {
737 imported: n.orig.atom().into_owned(),
738 exported: n.exported.as_ref().unwrap_or(&n.orig).atom().into_owned(),
739 },
740 ));
741 }
742 }
743 }
744 }
745
746 fn visit_export_decl(&mut self, n: &ExportDecl) {
747 self.data.has_exports = true;
748
749 if self.comments.is_some() {
750 n.visit_children_with(self);
752 }
753
754 match &n.decl {
755 Decl::Class(n) => {
756 self.data
757 .exports
758 .insert(n.ident.sym.as_str().into(), n.ident.to_id());
759 }
760 Decl::Fn(n) => {
761 self.data
762 .exports
763 .insert(n.ident.sym.as_str().into(), n.ident.to_id());
764 }
765 Decl::Var(..) | Decl::Using(..) => {
766 let ids: Vec<Id> = find_pat_ids(&n.decl);
767 for id in ids {
768 self.data.exports.insert(id.0.as_str().into(), id);
769 }
770 }
771 _ => {}
772 }
773 }
774
775 fn visit_export_default_decl(&mut self, n: &ExportDefaultDecl) {
776 self.data.has_exports = true;
777
778 if self.comments.is_some() {
779 n.visit_children_with(self);
781 }
782
783 self.data.exports.insert(
784 rcstr!("default"),
785 match &n.decl {
788 DefaultDecl::Class(ClassExpr { ident, .. })
789 | DefaultDecl::Fn(FnExpr { ident, .. }) => ident.as_ref().map_or_else(
790 || {
791 (
792 magic_identifier::mangle("default export").into(),
793 SyntaxContext::empty(),
794 )
795 },
796 |ident| ident.to_id(),
797 ),
798 DefaultDecl::TsInterfaceDecl(_) => {
799 (
801 magic_identifier::mangle("default export").into(),
802 SyntaxContext::empty(),
803 )
804 }
805 },
806 );
807 }
808
809 fn visit_export_default_expr(&mut self, n: &ExportDefaultExpr) {
810 self.data.has_exports = true;
811
812 if self.comments.is_some() {
813 n.visit_children_with(self);
815 }
816
817 self.data.exports.insert(
818 rcstr!("default"),
819 (
820 magic_identifier::mangle("default export").into(),
822 SyntaxContext::empty(),
823 ),
824 );
825 }
826
827 fn visit_export_named_specifier(&mut self, n: &ExportNamedSpecifier) {
828 let ModuleExportName::Ident(local) = &n.orig else {
829 unreachable!("string reexports should have been already handled in visit_named_export");
832 };
833 let exported = n.exported.as_ref().unwrap_or(&n.orig);
834 self.data
835 .exports
836 .insert(exported.atom().as_str().into(), local.to_id());
837 }
838
839 fn visit_export_default_specifier(&mut self, n: &ExportDefaultSpecifier) {
840 self.data
841 .exports
842 .insert(rcstr!("default"), n.exported.to_id());
843 }
844
845 fn visit_program(&mut self, m: &Program) {
846 self.data.has_top_level_await = has_top_level_await(m).is_some();
847 self.data.strict = match m {
848 Program::Module(module) => module
849 .body
850 .iter()
851 .take_while(|s| s.directive_continue())
852 .any(IsDirective::is_use_strict),
853 Program::Script(script) => script
854 .body
855 .iter()
856 .take_while(|s| s.directive_continue())
857 .any(IsDirective::is_use_strict),
858 };
859
860 m.visit_children_with(self);
861 }
862
863 fn visit_stmt(&mut self, n: &Stmt) {
864 if self.comments.is_some() {
865 n.visit_children_with(self);
867 }
868 }
869
870 fn visit_call_expr(&mut self, n: &CallExpr) {
884 if let Some(comments) = self.comments {
886 let callee_span = match &n.callee {
887 Callee::Import(Import { span, .. }) => Some(*span),
888 Callee::Expr(e) => Some(e.span()),
889 _ => None,
890 };
891
892 if let Some(callee_span) = callee_span
893 && let Some(attributes) = parse_directives(comments, n.args.first())
894 {
895 self.data.attributes.insert(callee_span.lo, attributes);
896 }
897 }
898
899 n.visit_children_with(self);
900 }
901
902 fn visit_new_expr(&mut self, n: &NewExpr) {
903 if let Some(comments) = self.comments {
905 let callee_span = match &n.callee {
906 box Expr::Ident(Ident { sym, .. }) if sym == "Worker" => Some(n.span),
907 _ => None,
908 };
909
910 if let Some(callee_span) = callee_span
911 && let Some(attributes) = parse_directives(comments, n.args.iter().flatten().next())
912 {
913 self.data.attributes.insert(callee_span.lo, attributes);
914 }
915 }
916
917 n.visit_children_with(self);
918 }
919}
920
921fn parse_directives(
924 comments: &dyn Comments,
925 value: Option<&ExprOrSpread>,
926) -> Option<ImportAttributes> {
927 let value = value?;
928 let leading_comments = comments.get_leading(value.span_lo())?;
929
930 let mut ignore = None;
931 let mut optional = None;
932 let mut export_names = None;
933 let mut chunking_type = None;
934
935 for comment in leading_comments.iter() {
937 if let Some((directive, val)) = comment.text.trim().split_once(':') {
938 let val = val.trim();
939 match directive.trim() {
940 "webpackIgnore" | "turbopackIgnore" => match val {
941 "true" => ignore = Some(true),
942 "false" => ignore = Some(false),
943 _ => {}
944 },
945 "turbopackOptional" => match val {
946 "true" => optional = Some(true),
947 "false" => optional = Some(false),
948 _ => {}
949 },
950 "webpackExports" | "turbopackExports" => {
951 export_names = Some(parse_export_names(val));
952 }
953 "turbopackChunkingType" => {
954 chunking_type = parse_chunking_type_annotation(value.span(), val);
955 }
956 _ => {} }
958 }
959 }
960
961 if ignore.is_some() || optional.is_some() || export_names.is_some() || chunking_type.is_some() {
963 Some(ImportAttributes {
964 ignore: ignore.unwrap_or(false),
965 optional: optional.unwrap_or(false),
966 export_names,
967 chunking_type,
968 })
969 } else {
970 None
971 }
972}
973
974fn parse_export_names(val: &str) -> SmallVec<[RcStr; 1]> {
980 let val = val.trim();
981
982 if let Ok(names) = serde_json::from_str::<Vec<String>>(val) {
984 return names.into_iter().map(|s| s.into()).collect();
985 }
986
987 if let Ok(name) = serde_json::from_str::<String>(val) {
989 return SmallVec::from_buf([name.into()]);
990 }
991
992 if !val.is_empty() {
994 return SmallVec::from_buf([val.into()]);
995 }
996
997 SmallVec::new()
998}
999
1000fn parse_with(with: Option<&ObjectLit>) -> Option<ImportedSymbol> {
1001 find_turbopack_part_id_in_asserts(with?).map(|v| match v {
1002 PartId::Internal(index, true) => ImportedSymbol::PartEvaluation(index),
1003 PartId::Internal(index, false) => ImportedSymbol::Part(index),
1004 PartId::ModuleEvaluation => ImportedSymbol::ModuleEvaluation,
1005 PartId::Export(e) => ImportedSymbol::Symbol(e.as_str().into()),
1006 PartId::Exports => ImportedSymbol::Exports,
1007 })
1008}
1009
1010fn get_import_symbol_from_import(specifier: &ImportSpecifier) -> ImportedSymbol {
1011 match specifier {
1012 ImportSpecifier::Named(ImportNamedSpecifier {
1013 local, imported, ..
1014 }) => ImportedSymbol::Symbol(match imported {
1015 Some(imported) => imported.atom().into_owned(),
1016 _ => local.sym.clone(),
1017 }),
1018 ImportSpecifier::Default(..) => ImportedSymbol::Symbol(atom!("default")),
1019 ImportSpecifier::Namespace(..) => ImportedSymbol::Exports,
1020 }
1021}
1022
1023fn get_import_symbol_from_export(specifier: &ExportSpecifier) -> ImportedSymbol {
1024 match specifier {
1025 ExportSpecifier::Named(ExportNamedSpecifier { orig, .. }) => {
1026 ImportedSymbol::Symbol(orig.atom().into_owned())
1027 }
1028 ExportSpecifier::Default(..) => ImportedSymbol::Symbol(atom!("default")),
1029 ExportSpecifier::Namespace(..) => ImportedSymbol::Exports,
1030 }
1031}
1032
1033#[cfg(test)]
1034mod tests {
1035 use swc_core::{atoms::Atom, common::DUMMY_SP, ecma::ast::*};
1036
1037 use super::*;
1038
1039 fn str_lit(s: &str) -> Box<Expr> {
1041 Box::new(Expr::Lit(Lit::Str(Str {
1042 span: DUMMY_SP,
1043 value: Atom::from(s).into(),
1044 raw: None,
1045 })))
1046 }
1047
1048 fn ident_key(s: &str) -> PropName {
1050 PropName::Ident(IdentName {
1051 span: DUMMY_SP,
1052 sym: Atom::from(s),
1053 })
1054 }
1055
1056 fn kv_prop(key: PropName, value: Box<Expr>) -> PropOrSpread {
1058 PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { key, value })))
1059 }
1060
1061 #[test]
1062 fn test_parse_turbopack_loader_annotation() {
1063 let with = ObjectLit {
1065 span: DUMMY_SP,
1066 props: vec![kv_prop(ident_key("turbopackLoader"), str_lit("raw-loader"))],
1067 };
1068
1069 let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1070 assert!(annotations.has_turbopack_loader());
1071
1072 let loader = annotations.turbopack_loader().unwrap();
1073 assert_eq!(loader.loader.as_str(), "raw-loader");
1074 assert!(loader.options.is_empty());
1075 }
1076
1077 #[test]
1078 fn test_parse_turbopack_loader_with_options() {
1079 let with = ObjectLit {
1081 span: DUMMY_SP,
1082 props: vec![
1083 kv_prop(ident_key("turbopackLoader"), str_lit("my-loader")),
1084 kv_prop(
1085 ident_key("turbopackLoaderOptions"),
1086 str_lit(r#"{"flag":true}"#),
1087 ),
1088 ],
1089 };
1090
1091 let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1092 assert!(annotations.has_turbopack_loader());
1093
1094 let loader = annotations.turbopack_loader().unwrap();
1095 assert_eq!(loader.loader.as_str(), "my-loader");
1096 assert_eq!(loader.options["flag"], serde_json::Value::Bool(true));
1097 }
1098
1099 #[test]
1100 fn test_parse_without_turbopack_loader() {
1101 let with = ObjectLit {
1103 span: DUMMY_SP,
1104 props: vec![kv_prop(ident_key("type"), str_lit("json"))],
1105 };
1106
1107 let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1108 assert!(!annotations.has_turbopack_loader());
1109 assert!(annotations.module_type().is_some());
1110 }
1111
1112 #[test]
1113 fn test_parse_empty_with() {
1114 let annotations = ImportAnnotations::parse(None);
1115 assert!(annotations.is_none());
1116 }
1117}