1use std::{
2 borrow::Cow,
3 collections::{BTreeMap, hash_map::Entry},
4 fmt::Display,
5 sync::{Arc, LazyLock},
6};
7
8use anyhow::{Context, Result};
9use auto_hash_map::AutoSet;
10use rustc_hash::{FxHashMap, FxHashSet};
11use smallvec::SmallVec;
12use swc_core::{
13 atoms::Wtf8Atom,
14 common::{BytePos, GLOBALS, Mark, Span, Spanned, SyntaxContext, comments::Comments},
15 ecma::{
16 ast::*,
17 atoms::{Atom, atom},
18 utils::{IsDirective, find_pat_ids},
19 visit::{Visit, VisitWith},
20 },
21};
22use turbo_frozenmap::FrozenMap;
23use turbo_rcstr::{RcStr, rcstr};
24use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc};
25use turbopack_core::{
26 loader::WebpackLoaderItem,
27 resolve::{ExportUsage, ImportUsage},
28};
29
30use super::{JsValue, ModuleValue, top_level_await::has_top_level_await};
31use crate::{
32 SpecifiedModuleType,
33 analyzer::{
34 Bump, ConstantString, ConstantValue, ObjectPart,
35 cjs_ast::is_global,
36 graph::{AssignmentScope, AssignmentScopes, EvalContext},
37 is_unresolved, is_unresolved_id,
38 },
39 magic_identifier::{MAGIC_IDENTIFIER_DEFAULT_EXPORT, MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM},
40 module_fragments::{PartId, find_turbopack_part_id_in_asserts},
41 references::{
42 cross_module_constants::is_import_name_eligible_for_exports,
43 esm::{EsmAssetReference, EsmExport, Liveness},
44 util::{SpecifiedChunkingType, parse_chunking_type_annotation},
45 },
46 utils::{extract_name_from_member_prop, extract_names_from_object_pat, unparen},
47};
48
49#[turbo_tasks::value]
50#[derive(Default, Debug, Clone, Hash)]
51pub struct ImportAnnotations {
52 #[turbo_tasks(trace_ignore)]
54 #[bincode(with_serde)]
55 map: BTreeMap<Wtf8Atom, Wtf8Atom>,
56
57 #[turbo_tasks(trace_ignore)]
60 #[bincode(with_serde)]
61 turbopack_loader: Option<WebpackLoaderItem>,
62 turbopack_rename_as: Option<RcStr>,
63 turbopack_module_type: Option<RcStr>,
64 chunking_type: Option<SpecifiedChunkingType>,
65
66 turbopack_constants: Option<bool>,
67}
68
69static ANNOTATION_TRANSITION: LazyLock<Wtf8Atom> =
71 LazyLock::new(|| crate::annotations::ANNOTATION_TRANSITION.into());
72
73static ATTRIBUTE_MODULE_TYPE: LazyLock<Wtf8Atom> = LazyLock::new(|| atom!("type").into());
75
76impl ImportAnnotations {
77 pub fn parse(with: Option<&ObjectLit>) -> Option<ImportAnnotations> {
78 let with = with?;
79
80 let mut map = BTreeMap::new();
81 let mut turbopack_loader_name: Option<RcStr> = None;
82 let mut turbopack_loader_options: serde_json::Map<String, serde_json::Value> =
83 serde_json::Map::new();
84 let mut turbopack_rename_as: Option<RcStr> = None;
85 let mut turbopack_module_type: Option<RcStr> = None;
86 let mut chunking_type: Option<SpecifiedChunkingType> = None;
87 let mut turbopack_constants: Option<bool> = None;
88 for prop in &with.props {
89 let Some(kv) = prop.as_prop().and_then(|p| p.as_key_value()) else {
90 continue;
91 };
92
93 let key_str = match &kv.key {
94 PropName::Ident(ident) => Cow::Borrowed(ident.sym.as_str()),
95 PropName::Str(str) => str.value.to_string_lossy(),
96 _ => continue,
97 };
98
99 match &*key_str {
101 "turbopackLoader" => {
102 if let Some(Lit::Str(s)) = kv.value.as_lit() {
103 turbopack_loader_name =
104 Some(RcStr::from(s.value.to_string_lossy().into_owned()));
105 }
106 }
107 "turbopackLoaderOptions" => {
108 if let Some(Lit::Str(s)) = kv.value.as_lit() {
109 let json_str = s.value.to_string_lossy();
110 if let Ok(serde_json::Value::Object(map)) = serde_json::from_str(&json_str)
111 {
112 turbopack_loader_options = map;
113 }
114 }
115 }
116 "turbopackAs" => {
117 if let Some(Lit::Str(s)) = kv.value.as_lit() {
118 turbopack_rename_as =
119 Some(RcStr::from(s.value.to_string_lossy().into_owned()));
120 }
121 }
122 "turbopackModuleType" => {
123 if let Some(Lit::Str(s)) = kv.value.as_lit() {
124 turbopack_module_type =
125 Some(RcStr::from(s.value.to_string_lossy().into_owned()));
126 }
127 }
128 "turbopack-chunking-type" => {
129 if let Some(Lit::Str(s)) = kv.value.as_lit() {
130 chunking_type = parse_chunking_type_annotation(
131 kv.value.span(),
132 &s.value.to_string_lossy(),
133 );
134 }
135 }
136 "turbopackConstants" => {
137 if let Some(Lit::Str(s)) = kv.value.as_lit() {
138 turbopack_constants = Some(s.value.to_string_lossy() == "true");
139 }
140 }
141 _ => {
142 if let Some(Lit::Str(str)) = kv.value.as_lit() {
144 let key: Wtf8Atom = match &kv.key {
145 PropName::Ident(ident) => ident.sym.clone().into(),
146 PropName::Str(s) => s.value.clone(),
147 _ => continue,
148 };
149 map.insert(key, str.value.clone());
150 }
151 }
152 }
153 }
154
155 let turbopack_loader = turbopack_loader_name.map(|name| WebpackLoaderItem {
156 loader: name,
157 options: turbopack_loader_options,
158 });
159
160 if !map.is_empty()
161 || turbopack_loader.is_some()
162 || turbopack_rename_as.is_some()
163 || turbopack_module_type.is_some()
164 || chunking_type.is_some()
165 || turbopack_constants.is_some()
166 {
167 Some(ImportAnnotations {
168 map,
169 turbopack_loader,
170 turbopack_rename_as,
171 turbopack_module_type,
172 chunking_type,
173 turbopack_constants,
174 })
175 } else {
176 None
177 }
178 }
179
180 pub fn parse_dynamic(with: &JsValue<'_>) -> Option<ImportAnnotations> {
181 let mut map = BTreeMap::new();
182
183 let JsValue::Object { parts, .. } = with else {
184 return None;
185 };
186
187 for part in parts.iter() {
188 let ObjectPart::KeyValue(key, value) = part else {
189 continue;
190 };
191 let (
192 JsValue::Constant(ConstantValue::Str(key)),
193 JsValue::Constant(ConstantValue::Str(value)),
194 ) = (key, value)
195 else {
196 continue;
197 };
198
199 map.insert(
200 key.as_atom().into_owned().into(),
201 value.as_atom().into_owned().into(),
202 );
203 }
204
205 if !map.is_empty() {
206 Some(ImportAnnotations {
207 map,
208 turbopack_loader: None,
209 turbopack_rename_as: None,
210 turbopack_module_type: None,
211 chunking_type: None,
212 turbopack_constants: None,
213 })
214 } else {
215 None
216 }
217 }
218
219 pub fn transition(&self) -> Option<Cow<'_, str>> {
221 self.get(&ANNOTATION_TRANSITION)
222 .map(|v| v.to_string_lossy())
223 }
224
225 pub fn chunking_type(&self) -> Option<SpecifiedChunkingType> {
227 self.chunking_type
228 }
229
230 pub fn module_type(&self) -> Option<&Wtf8Atom> {
232 self.get(&ATTRIBUTE_MODULE_TYPE)
233 }
234
235 pub fn turbopack_loader(&self) -> Option<&WebpackLoaderItem> {
237 self.turbopack_loader.as_ref()
238 }
239
240 pub fn turbopack_rename_as(&self) -> Option<&RcStr> {
242 self.turbopack_rename_as.as_ref()
243 }
244
245 pub fn turbopack_module_type(&self) -> Option<&RcStr> {
247 self.turbopack_module_type.as_ref()
248 }
249
250 pub fn has_turbopack_loader(&self) -> bool {
252 self.turbopack_loader.is_some()
253 }
254
255 pub fn turbopack_constants(&self) -> Option<bool> {
257 self.turbopack_constants
258 }
259
260 pub fn get(&self, key: &Wtf8Atom) -> Option<&Wtf8Atom> {
261 self.map.get(key)
262 }
263}
264
265impl Display for ImportAnnotations {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 let mut it = self.map.iter();
268 if let Some((k, v)) = it.next() {
269 write!(f, "{{ {}: {}", k.to_string_lossy(), v.to_string_lossy())?
270 } else {
271 return f.write_str("{}");
272 };
273 for (k, v) in it {
274 write!(f, ", {}: {}", k.to_string_lossy(), v.to_string_lossy())?
275 }
276 f.write_str(" }")
277 }
278}
279
280#[derive(Clone, Debug)]
281pub enum DeclUsage {
282 SideEffects,
283 Bindings(FxHashSet<Id>),
284}
285impl Default for DeclUsage {
286 fn default() -> Self {
287 DeclUsage::Bindings(Default::default())
288 }
289}
290impl DeclUsage {
291 fn add_usage(&mut self, user: &Id) {
292 match self {
293 Self::Bindings(set) => {
294 set.insert(user.clone());
295 }
296 Self::SideEffects => {}
297 }
298 }
299 fn make_side_effects(&mut self) {
300 *self = Self::SideEffects;
301 }
302}
303
304#[derive(Default, Debug)]
305pub(crate) struct ProgramDeclUsage {
306 pub(crate) decl_usages: FxHashMap<Id, DeclUsage>,
308 pub(crate) import_usages: FxHashMap<usize, DeclUsage>,
310 pub(crate) named_reexports: FxHashMap<usize, AutoSet<RcStr>>,
312 pub(crate) exports: FxHashMap<RcStr, Id>,
314}
315impl ProgramDeclUsage {
316 fn compute_import_usage(&self) -> FxHashMap<usize, ImportUsage> {
317 let mut import_usage =
318 FxHashMap::with_capacity_and_hasher(self.import_usages.len(), Default::default());
319 for (reference, usage) in &self.import_usages {
320 if let DeclUsage::Bindings(ids) = usage {
322 let mut visited = ids.clone();
324 let mut stack = ids.iter().collect::<Vec<_>>();
325 let mut has_global_usage = false;
326 while let Some(id) = stack.pop() {
327 match self.decl_usages.get(id) {
328 Some(DeclUsage::SideEffects) => {
329 has_global_usage = true;
330 break;
331 }
332 Some(DeclUsage::Bindings(callers)) => {
333 for caller in callers {
334 if visited.insert(caller.clone()) {
335 stack.push(caller);
336 }
337 }
338 }
339 _ => {}
340 }
341 }
342
343 import_usage.insert(
345 *reference,
346 if has_global_usage {
347 ImportUsage::TopLevel
348 } else {
349 ImportUsage::Exports(
350 self.exports
351 .iter()
352 .filter(|(_, id)| visited.contains(*id))
353 .map(|(exported, _)| exported.clone())
354 .collect(),
355 )
356 },
357 );
358 }
359 }
360 for (reference, names) in &self.named_reexports {
362 let usage = match import_usage.get(reference) {
363 Some(ImportUsage::TopLevel) => continue,
364 Some(ImportUsage::Exports(existing)) => ImportUsage::Exports(
368 existing
369 .iter()
370 .cloned()
371 .chain(names.iter().cloned())
372 .collect(),
373 ),
374 None => ImportUsage::Exports(names.iter().cloned().collect()),
375 };
376 import_usage.insert(*reference, usage);
377 }
378 import_usage
379 }
380}
381
382#[derive(Debug)]
385pub enum Export {
386 LocalBinding(RcStr, bool),
390 ImportedBinding(usize, RcStr, bool),
394 ImportedNamespace(usize),
396 Error,
398}
399
400#[derive(Default, Debug)]
402pub(crate) struct ImportMap {
403 imports: FxIndexMap<Id, (usize, Atom)>,
405
406 namespace_imports: FxIndexMap<Id, usize>,
408
409 pub(crate) exports: BTreeMap<RcStr, Export>,
411
412 reexport_namespaces: Vec<usize>,
414
415 references: FxIndexSet<ImportMapReference>,
417
418 has_imports: bool,
421
422 has_exports: bool,
425
426 has_top_level_await: bool,
428
429 pub(crate) strict: bool,
431
432 attributes: FxHashMap<BytePos, ImportAttributes>,
439
440 full_star_imports: FxHashSet<Wtf8Atom>,
443
444 pub(super) assignment_scopes: FxHashMap<Id, AssignmentScopes>,
447
448 pub(crate) import_usage: FxHashMap<usize, ImportUsage>,
449
450 pub(crate) exports_ids: FxHashMap<RcStr, (Id, Span)>,
452
453 cjs_imports: CjsImports,
456}
457
458#[derive(Default, Debug)]
459pub(crate) struct CjsImports {
460 pub(crate) resolved: FxHashMap<BytePos, ExportUsage>,
462
463 pub(crate) bindings: FxHashMap<Id, BytePos>,
465}
466
467#[derive(Debug)]
472pub struct ImportAttributes {
473 pub ignore: bool,
484 pub optional: bool,
494 pub export_names: Option<SmallVec<[RcStr; 1]>>,
508 pub chunking_type: Option<SpecifiedChunkingType>,
517}
518
519impl ImportAttributes {
520 pub const fn empty() -> Self {
521 ImportAttributes {
522 ignore: false,
523 optional: false,
524 export_names: None,
525 chunking_type: None,
526 }
527 }
528
529 pub fn empty_ref() -> &'static Self {
530 static DEFAULT_VALUE: ImportAttributes = ImportAttributes::empty();
532 &DEFAULT_VALUE
533 }
534}
535
536impl Default for ImportAttributes {
537 fn default() -> Self {
538 ImportAttributes::empty()
539 }
540}
541
542impl Default for &ImportAttributes {
543 fn default() -> Self {
544 ImportAttributes::empty_ref()
545 }
546}
547
548#[derive(Debug, Clone, PartialEq, Eq, Hash)]
549pub(crate) enum ImportedSymbol {
550 ModuleEvaluation,
551 Symbol(Atom),
552 Exports,
553 Part(u32),
554 PartEvaluation(u32),
555}
556
557#[derive(Debug, Clone, PartialEq, Eq, Hash)]
558pub(crate) struct ImportMapReference {
559 pub module_path: Wtf8Atom,
560 pub imported_symbol: ImportedSymbol,
561 pub annotations: Option<Arc<ImportAnnotations>>,
562 pub span: Span,
563}
564
565impl ImportMap {
566 pub fn is_esm(&self, specified_type: SpecifiedModuleType) -> bool {
567 if self.has_exports {
568 return true;
569 }
570
571 match specified_type {
572 SpecifiedModuleType::Automatic => {
573 self.has_exports || self.has_imports || self.has_top_level_await
574 }
575 SpecifiedModuleType::CommonJs => false,
576 SpecifiedModuleType::EcmaScript => true,
577 }
578 }
579
580 pub fn is_cjs(&self, specified_type: SpecifiedModuleType) -> bool {
581 !self.is_esm(specified_type)
582 }
583
584 pub fn get_import_for_idx<'a>(
585 &self,
586 arena: &'a Bump,
587 esm_reference_idx: usize,
588 export: Option<ConstantString>,
589 ) -> JsValue<'a> {
590 let r = &self.references[esm_reference_idx];
591 if let Some(export) = export {
592 JsValue::member(
593 arena,
594 JsValue::Module(ModuleValue {
595 module: r.module_path.clone(),
596 annotations: r.annotations.clone(),
597 reference: Some((esm_reference_idx as u32).into()),
598 analyze_for_constants: is_import_name_eligible_for_exports(export.as_str()),
599 }),
600 JsValue::Constant(ConstantValue::Str(export)),
601 )
602 } else {
603 JsValue::Module(ModuleValue {
604 module: r.module_path.clone(),
605 annotations: r.annotations.clone(),
606 reference: Some((esm_reference_idx as u32).into()),
607 analyze_for_constants: false,
608 })
609 }
610 }
611
612 pub fn get_import<'a>(&self, arena: &'a Bump, id: &Id) -> Option<JsValue<'a>> {
613 if let Some((i, i_sym)) = self.imports.get(id) {
614 return Some(self.get_import_for_idx(arena, *i, Some(i_sym.clone().into())));
615 }
616 if let Some(i) = self.namespace_imports.get(id) {
617 return Some(self.get_import_for_idx(arena, *i, None));
618 }
619 None
620 }
621
622 pub fn get_attributes(&self, span: Span) -> &ImportAttributes {
623 self.attributes.get(&span.lo).unwrap_or_default()
624 }
625
626 pub fn get_annotations(&self, idx: usize) -> Option<&Arc<ImportAnnotations>> {
627 self.references
628 .get_index(idx)
629 .and_then(|r| r.annotations.as_ref())
630 }
631
632 pub fn get_binding(&self, id: &Id) -> Option<(usize, Option<&Atom>)> {
633 if let Some((i, i_sym)) = self.imports.get(id) {
634 return Some((*i, Some(i_sym)));
635 }
636 if let Some(i) = self.namespace_imports.get(id) {
637 return Some((*i, None));
638 }
639 None
640 }
641
642 pub fn references(&self) -> impl ExactSizeIterator<Item = &ImportMapReference> {
643 self.references.iter()
644 }
645
646 pub fn reexports_reference_idxs(&self) -> impl Iterator<Item = usize> {
647 self.exports
648 .values()
649 .filter_map(|value| match value {
650 Export::ImportedBinding(i, ..) | Export::ImportedNamespace(i) => Some(*i),
651 Export::LocalBinding(..) | Export::Error => None,
652 })
653 .chain(self.reexport_namespaces.iter().copied())
654 }
655
656 pub fn as_esm_exports(
657 &self,
658 import_references: &[ResolvedVc<EsmAssetReference>],
659 eval_context: &EvalContext,
660 ) -> Result<FrozenMap<RcStr, EsmExport>> {
661 Ok(FrozenMap::from(
662 self.exports
663 .iter()
664 .map(|(name, value)| {
665 let value = match value {
666 Export::LocalBinding(local, is_fake_esm) => EsmExport::LocalBinding(
667 local.clone(),
668 if *is_fake_esm {
669 Liveness::Mutable
671 } else {
672 eval_context.imports.get_export_ident_liveness(
673 self.exports_ids
674 .get(name)
675 .cloned()
676 .with_context(|| {
677 format!(
678 "Exported binding {name} not found in exports_ids"
679 )
680 })?
681 .0,
682 eval_context.unresolved_mark,
683 )
684 },
685 ),
686 Export::ImportedBinding(i, name, is_fake_esm) => {
687 EsmExport::ImportedBinding(
688 ResolvedVc::upcast(import_references[*i]),
689 name.clone(),
690 *is_fake_esm,
691 )
692 }
693 Export::ImportedNamespace(i) => {
694 EsmExport::ImportedNamespace(ResolvedVc::upcast(import_references[*i]))
695 }
696 Export::Error => EsmExport::Error,
697 };
698 Ok((name.clone(), value))
699 })
700 .collect::<Result<Vec<_>>>()?,
701 ))
702 }
703
704 pub fn reexport_namespaces(&self) -> impl ExactSizeIterator<Item = usize> {
705 self.reexport_namespaces.iter().copied()
706 }
707
708 pub fn get_export_ident_liveness(&self, id: Id, unresolved_mark: Mark) -> Liveness {
711 if let Some(assignment_scopes) = self.assignment_scopes.get(&id) {
712 if *assignment_scopes != AssignmentScopes::AllInModuleEvalScope {
714 Liveness::Live
715 } else {
716 Liveness::Constant
717 }
718 } else {
719 debug_assert!(
724 self.imports.contains_key(&id)
725 || self.namespace_imports.contains_key(&id)
726 || !GLOBALS.is_set()
727 || is_unresolved_id(&id, unresolved_mark),
728 "export ident {id:?} without an assignment scope should be a free variable or an \
729 imported variable"
730 );
731
732 Liveness::Live
733 }
734 }
735
736 pub(super) fn analyze(
738 unresolved_mark: Mark,
739 m: &Program,
740 comments: Option<&dyn Comments>,
741 ) -> Self {
742 let mut data = ImportMap::default();
743 let mut analyzer = Analyzer {
744 unresolved_mark,
745 data: &mut data,
746 comments,
747 namespace_imports_to_specifier: FxIndexMap::default(),
748 state: Default::default(),
749 program_decl_usage: Default::default(),
750 };
751
752 if let Program::Module(m) = m {
754 for stmt in &m.body {
755 match stmt {
756 ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => {
757 if import.type_only {
758 continue;
759 }
760 analyzer.data.has_imports = true;
761 let annotations = ImportAnnotations::parse(import.with.as_deref());
762 let internal_symbol = parse_with(import.with.as_deref());
763 if internal_symbol.is_none() {
764 analyzer.ensure_reference(
765 import.span,
766 import.src.value.clone(),
767 ImportedSymbol::ModuleEvaluation,
768 annotations.clone(),
769 );
770 }
771
772 for s in &import.specifiers {
773 if s.is_type_only() {
774 continue;
775 }
776 let symbol = internal_symbol
777 .clone()
778 .unwrap_or_else(|| get_import_symbol_from_import(s));
779 let i = analyzer.ensure_reference(
780 import.span,
781 import.src.value.clone(),
782 symbol,
783 annotations.clone(),
784 );
785
786 let (local, orig_sym) = match s {
787 ImportSpecifier::Namespace(s) => {
788 analyzer
789 .namespace_imports_to_specifier
790 .insert(s.local.to_id(), import.src.value.clone());
791 analyzer.data.namespace_imports.insert(s.local.to_id(), i);
792 continue;
793 }
794 ImportSpecifier::Default(s) => (s.local.to_id(), atom!("default")),
795 ImportSpecifier::Named(s) => match &s.imported {
796 Some(imported) => {
797 (s.local.to_id(), imported.atom().into_owned())
798 }
799 _ => (s.local.to_id(), s.local.sym.clone()),
800 },
801 };
802 analyzer.data.imports.insert(local, (i, orig_sym));
803 }
804 if import.specifiers.is_empty()
805 && let Some(internal_symbol) = internal_symbol
806 {
807 analyzer.ensure_reference(
808 import.span,
809 import.src.value.clone(),
810 internal_symbol,
811 annotations,
812 );
813 }
814 }
815 ModuleItem::ModuleDecl(ModuleDecl::ExportAll(export)) => {
818 if export.type_only {
819 continue;
820 }
821 let annotations = ImportAnnotations::parse(export.with.as_deref());
822 analyzer.ensure_reference(
823 export.span,
824 export.src.value.clone(),
825 ImportedSymbol::ModuleEvaluation,
826 annotations.clone(),
827 );
828 }
829 ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
830 if export.type_only {
831 continue;
832 }
833 if let Some(ref src) = export.src {
834 let annotations = ImportAnnotations::parse(export.with.as_deref());
835 let internal_symbol = parse_with(export.with.as_deref());
836 if internal_symbol.is_none() || export.specifiers.is_empty() {
837 analyzer.ensure_reference(
838 export.span,
839 src.value.clone(),
840 ImportedSymbol::ModuleEvaluation,
841 annotations.clone(),
842 );
843 }
844 }
845 }
846 _ => (),
847 }
848 }
849 }
850
851 m.visit_with(&mut analyzer);
852
853 data.import_usage = analyzer.program_decl_usage.compute_import_usage();
854
855 data
856 }
857
858 pub(crate) fn should_import_all(&self, esm_reference_index: usize) -> bool {
859 let r = &self.references[esm_reference_index];
860
861 self.full_star_imports.contains(&r.module_path)
862 }
863
864 pub(crate) fn cjs_imports(&self) -> &CjsImports {
865 &self.cjs_imports
866 }
867}
868
869mod analyzer_state {
870 use swc_core::ecma::ast::{Id, Ident};
871
872 use super::Analyzer;
873
874 #[derive(Default)]
875 pub(super) struct AnalyzerState {
876 is_in_fn: bool,
877 cur_top_level_decl_name: Option<Id>,
878 }
879
880 impl AnalyzerState {
881 pub(super) fn cur_top_level_decl_name(&self) -> &Option<Id> {
883 &self.cur_top_level_decl_name
884 }
885
886 pub(super) fn is_in_fn(&self) -> bool {
888 self.is_in_fn
889 }
890 }
891
892 impl Analyzer<'_> {
893 pub(super) fn enter_top_level_decl<T>(
895 &mut self,
896 name: &Ident,
897 visitor: impl FnOnce(&mut Self) -> T,
898 ) -> T {
899 let is_top_level_fn = self.state.cur_top_level_decl_name.is_none();
900 if is_top_level_fn {
901 self.state.cur_top_level_decl_name = Some(name.to_id());
902 }
903 let result = visitor(self);
904 if is_top_level_fn {
905 self.state.cur_top_level_decl_name = None;
906 }
907 result
908 }
909
910 pub(super) fn enter_fn<T>(&mut self, visitor: impl FnOnce(&mut Self) -> T) -> T {
912 let old_is_in_fn = self.state.is_in_fn;
913 self.state.is_in_fn = true;
914 let result = visitor(self);
915 self.state.is_in_fn = old_is_in_fn;
916 result
917 }
918 }
919}
920
921struct Analyzer<'a> {
922 unresolved_mark: Mark,
923 data: &'a mut ImportMap,
924 comments: Option<&'a dyn Comments>,
925 namespace_imports_to_specifier: FxIndexMap<Id, Wtf8Atom>,
928
929 program_decl_usage: ProgramDeclUsage,
930
931 state: analyzer_state::AnalyzerState,
932}
933
934impl Analyzer<'_> {
935 fn ensure_reference(
936 &mut self,
937 span: Span,
938 module_path: Wtf8Atom,
939 imported_symbol: ImportedSymbol,
940 annotations: Option<ImportAnnotations>,
941 ) -> usize {
942 let r = ImportMapReference {
943 module_path,
944 imported_symbol,
945 span,
946 annotations: annotations.map(Arc::new),
947 };
948 if let Some(i) = self.data.references.get_index_of(&r) {
949 i
950 } else {
951 let i = self.data.references.len();
952 self.data.references.insert(r);
953 i
954 }
955 }
956
957 fn register_assignment_scope(&mut self, id: Id) {
958 let scope = if self.state.is_in_fn() {
959 AssignmentScope::Function
960 } else {
961 AssignmentScope::ModuleEval
962 };
963
964 match self.data.assignment_scopes.entry(id) {
965 Entry::Occupied(mut e) => {
966 *e.get_mut() = e.get().merge(scope);
967 }
968 Entry::Vacant(e) => {
969 e.insert(AssignmentScopes::new(scope));
970 }
971 }
972 }
973
974 fn record_require_usage_var(&mut self, n: &VarDeclarator) {
976 let Some(init) = &n.init else {
977 return;
978 };
979 let Some(call) = as_require_call(init, self.unresolved_mark) else {
980 return;
981 };
982 match &n.name {
983 Pat::Ident(binding) => {
984 self.data
985 .cjs_imports
986 .bindings
987 .insert(binding.id.to_id(), call.span.lo);
988 }
989 Pat::Object(_) => {
990 let usage = match extract_names_from_object_pat(&n.name) {
991 Some(names) if names.is_empty() => ExportUsage::Evaluation,
993 Some(names) => ExportUsage::PartialNamespaceObject(names),
994 None => ExportUsage::All,
995 };
996 self.data.cjs_imports.resolved.insert(call.span.lo, usage);
997 }
998 _ => {
999 self.data
1000 .cjs_imports
1001 .resolved
1002 .insert(call.span.lo, ExportUsage::All);
1003 }
1004 }
1005 }
1006}
1007
1008impl Visit for Analyzer<'_> {
1009 fn visit_import_decl(&mut self, _: &ImportDecl) {
1010 }
1012
1013 fn visit_export_all(&mut self, export: &ExportAll) {
1014 if export.type_only {
1015 return;
1016 }
1017
1018 let annotations = ImportAnnotations::parse(export.with.as_deref());
1019
1020 let symbol = parse_with(export.with.as_deref());
1021 let i = self.ensure_reference(
1022 export.span,
1023 export.src.value.clone(),
1024 symbol.unwrap_or(ImportedSymbol::Exports),
1025 annotations,
1026 );
1027 self.data.reexport_namespaces.push(i);
1028 self.data.has_exports = true;
1029 export.visit_children_with(self);
1030 }
1031
1032 fn visit_named_export(&mut self, export: &NamedExport) {
1033 if export.type_only {
1034 return;
1035 }
1036
1037 self.data.has_exports = true;
1038
1039 if let Some(ref src) = export.src {
1040 let annotations = ImportAnnotations::parse(export.with.as_deref());
1041 let internal_symbol = parse_with(export.with.as_deref());
1042
1043 for spec in export.specifiers.iter() {
1044 let symbol = internal_symbol
1045 .clone()
1046 .unwrap_or_else(|| get_import_symbol_from_export(spec));
1047
1048 let i = self.ensure_reference(
1049 export.span,
1050 src.value.clone(),
1051 symbol,
1052 annotations.clone(),
1053 );
1054
1055 let name = match spec {
1056 ExportSpecifier::Namespace(n) => {
1057 let name = RcStr::from(n.name.atom().as_str());
1058 self.data
1059 .exports
1060 .insert(name.clone(), Export::ImportedNamespace(i));
1061 name
1062 }
1063 ExportSpecifier::Default(d) => {
1064 let name = RcStr::from(d.exported.sym.as_str());
1065 self.data.exports.insert(
1066 name.clone(),
1067 Export::ImportedBinding(i, rcstr!("default"), false),
1068 );
1069 name
1070 }
1071 ExportSpecifier::Named(n) => {
1072 let name =
1073 RcStr::from(n.exported.as_ref().unwrap_or(&n.orig).atom().as_str());
1074 self.data.exports.insert(
1075 name.clone(),
1076 Export::ImportedBinding(i, RcStr::from(n.orig.atom().as_str()), false),
1077 );
1078 name
1079 }
1080 };
1081 self.program_decl_usage
1082 .named_reexports
1083 .entry(i)
1084 .or_default()
1085 .insert(name);
1086 }
1087 } else {
1088 for spec in export.specifiers.iter() {
1089 match spec {
1090 ExportSpecifier::Namespace(_) => {
1091 unreachable!(
1092 "ExportNamespaceSpecifier will not happen in combination with src == \
1093 None"
1094 );
1095 }
1096 ExportSpecifier::Default(_) => {
1097 unreachable!(
1098 "ExportDefaultSpecifier will not happen in combination with src == \
1099 None"
1100 );
1101 }
1102 ExportSpecifier::Named(ExportNamedSpecifier {
1103 orig,
1104 exported,
1105 is_type_only,
1106 ..
1107 }) => {
1108 if *is_type_only {
1109 continue;
1110 }
1111
1112 let is_fake_esm = export
1114 .with
1115 .as_deref()
1116 .map(find_turbopack_part_id_in_asserts)
1117 .is_some();
1118 let export = {
1119 let imported_binding = if let ModuleExportName::Ident(ident) = orig {
1120 self.data.get_binding(&ident.to_id())
1121 } else {
1122 None
1123 };
1124 if let Some((index, export)) = imported_binding {
1125 if let Some(export) = export {
1128 Export::ImportedBinding(
1129 index,
1130 RcStr::from(export.as_str()),
1131 is_fake_esm,
1132 )
1133 } else {
1134 Export::ImportedNamespace(index)
1135 }
1136 } else {
1137 Export::LocalBinding(RcStr::from(orig.atom().as_str()), is_fake_esm)
1138 }
1139 };
1140 self.data.exports.insert(
1141 RcStr::from(exported.as_ref().unwrap_or(orig).atom().as_str()),
1142 export,
1143 );
1144 }
1145 }
1146 }
1147 export.visit_children_with(self);
1148 }
1149 }
1150
1151 fn visit_export_decl(&mut self, n: &ExportDecl) {
1152 self.data.has_exports = true;
1153 match &n.decl {
1154 Decl::Class(n) => {
1155 let name = RcStr::from(n.ident.sym.as_str());
1156 self.data
1157 .exports
1158 .insert(name.clone(), Export::LocalBinding(name.clone(), false));
1159 self.data
1160 .exports_ids
1161 .insert(name.clone(), (n.ident.to_id(), n.ident.span));
1162 self.program_decl_usage
1163 .exports
1164 .insert(name, n.ident.to_id());
1165 }
1166 Decl::Fn(n) => {
1167 let name = RcStr::from(n.ident.sym.as_str());
1168 self.data
1169 .exports
1170 .insert(name.clone(), Export::LocalBinding(name.clone(), false));
1171 self.data
1172 .exports_ids
1173 .insert(name.clone(), (n.ident.to_id(), n.ident.span));
1174 self.program_decl_usage
1175 .exports
1176 .insert(name, n.ident.to_id());
1177 }
1178 Decl::Var(..) => {
1179 let ids: Vec<Id> = find_pat_ids(&n.decl);
1180 for id in ids {
1181 let name = RcStr::from(id.0.as_str());
1182 self.data
1183 .exports
1184 .insert(name.clone(), Export::LocalBinding(name.clone(), false));
1185 self.data
1186 .exports_ids
1187 .insert(name.clone(), (id.clone(), n.span));
1188 self.program_decl_usage.exports.insert(name, id);
1189 }
1190 }
1191 Decl::Using(_) => {
1192 unreachable!("using declarations can not be exported");
1194 }
1195 Decl::TsInterface(_) | Decl::TsTypeAlias(_) | Decl::TsEnum(_) | Decl::TsModule(_) => {
1196 }
1198 }
1199
1200 n.visit_children_with(self);
1201 }
1202
1203 fn visit_export_default_decl(&mut self, n: &ExportDefaultDecl) {
1204 self.data.has_exports = true;
1205
1206 let id = match &n.decl {
1207 DefaultDecl::Class(ClassExpr { ident, .. }) | DefaultDecl::Fn(FnExpr { ident, .. }) => {
1208 ident.as_ref().map_or_else(
1211 || {
1212 (
1213 MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM.clone(),
1214 SyntaxContext::empty(),
1215 )
1216 },
1217 |ident| ident.to_id(),
1218 )
1219 }
1220 DefaultDecl::TsInterfaceDecl(_) => {
1221 (
1223 MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM.clone(),
1224 SyntaxContext::empty(),
1225 )
1226 }
1227 };
1228
1229 self.register_assignment_scope(id.clone());
1230 self.data.exports.insert(
1231 rcstr!("default"),
1232 Export::LocalBinding(RcStr::from(id.0.as_str()), false),
1233 );
1234 self.data
1235 .exports_ids
1236 .insert(rcstr!("default"), (id.clone(), n.span));
1237 self.program_decl_usage
1238 .exports
1239 .insert(rcstr!("default"), id);
1240 n.visit_children_with(self);
1241 }
1242
1243 fn visit_export_default_expr(&mut self, n: &ExportDefaultExpr) {
1244 self.data.has_exports = true;
1245
1246 let default_id = (
1247 MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM.clone(),
1248 SyntaxContext::empty(),
1249 );
1250
1251 self.data.exports.insert(
1252 rcstr!("default"),
1253 Export::LocalBinding(MAGIC_IDENTIFIER_DEFAULT_EXPORT.clone(), false),
1254 );
1255 self.data.exports_ids.insert(
1256 rcstr!("default"),
1257 (
1258 (
1259 MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM.clone(),
1261 SyntaxContext::empty(),
1262 ),
1263 n.span,
1264 ),
1265 );
1266
1267 self.register_assignment_scope(default_id);
1268 n.visit_children_with(self);
1269 }
1270
1271 fn visit_export_named_specifier(&mut self, n: &ExportNamedSpecifier) {
1272 self.data.has_exports = true;
1273
1274 let ModuleExportName::Ident(local) = &n.orig else {
1275 unreachable!("exporting a string should be impossible")
1276 };
1277 let exported = RcStr::from(n.exported.as_ref().unwrap_or(&n.orig).atom().as_str());
1278 self.data
1279 .exports_ids
1280 .insert(exported.clone(), (local.to_id(), n.span));
1281 self.program_decl_usage
1282 .exports
1283 .insert(exported, local.to_id());
1284 n.visit_children_with(self);
1285 }
1286
1287 fn visit_export_default_specifier(&mut self, n: &ExportDefaultSpecifier) {
1288 self.data.has_exports = true;
1289
1290 self.data
1291 .exports_ids
1292 .insert(rcstr!("default"), (n.exported.to_id(), n.exported.span));
1293 n.visit_children_with(self);
1294 }
1295
1296 fn visit_program(&mut self, m: &Program) {
1297 self.data.has_top_level_await = has_top_level_await(m).is_some();
1298 self.data.strict = match m {
1299 Program::Module(module) => module
1300 .body
1301 .iter()
1302 .take_while(|s| s.directive_continue())
1303 .any(IsDirective::is_use_strict),
1304 Program::Script(script) => script
1305 .body
1306 .iter()
1307 .take_while(|s| s.directive_continue())
1308 .any(IsDirective::is_use_strict),
1309 };
1310
1311 m.visit_children_with(self);
1312 }
1313
1314 fn visit_call_expr(&mut self, n: &CallExpr) {
1328 if let Some(comments) = self.comments {
1329 let callee_span = match &n.callee {
1330 Callee::Import(Import { span, .. }) => Some(*span),
1331 Callee::Expr(e) => Some(e.span()),
1332 _ => None,
1333 };
1334
1335 if let Some(callee_span) = callee_span
1336 && let Some(attributes) = parse_directives(comments, n.args.first())
1337 {
1338 self.data.attributes.insert(callee_span.lo, attributes);
1339 }
1340 }
1341
1342 n.visit_children_with(self);
1343 }
1344
1345 fn visit_new_expr(&mut self, n: &NewExpr) {
1346 if let Some(comments) = self.comments {
1347 let callee_span = match &*n.callee {
1348 Expr::Ident(Ident { sym, .. }) if sym == "Worker" => Some(n.span),
1349 _ => None,
1350 };
1351
1352 if let Some(callee_span) = callee_span
1353 && let Some(attributes) = parse_directives(comments, n.args.iter().flatten().next())
1354 {
1355 self.data.attributes.insert(callee_span.lo, attributes);
1356 }
1357 }
1358
1359 n.visit_children_with(self);
1360 }
1361
1362 fn visit_getter_prop(&mut self, node: &GetterProp) {
1363 self.enter_fn(|this| {
1364 node.visit_children_with(this);
1365 });
1366 }
1367 fn visit_setter_prop(&mut self, node: &SetterProp) {
1368 self.enter_fn(|this| {
1369 node.visit_children_with(this);
1370 });
1371 }
1372 fn visit_function(&mut self, node: &Function) {
1373 self.enter_fn(|this| {
1374 node.visit_children_with(this);
1375 });
1376 }
1377 fn visit_constructor(&mut self, node: &Constructor) {
1378 self.enter_fn(|this| {
1379 node.visit_children_with(this);
1380 });
1381 }
1382 fn visit_arrow_expr(&mut self, node: &ArrowExpr) {
1383 self.enter_fn(|this| {
1384 node.visit_children_with(this);
1385 });
1386 }
1387
1388 fn visit_member_expr(&mut self, node: &MemberExpr) {
1389 if let Some(call) = as_require_call(&node.obj, self.unresolved_mark) {
1391 let usage = match extract_name_from_member_prop(&node.prop) {
1392 Some(names) => ExportUsage::PartialNamespaceObject(names),
1393 None => ExportUsage::All,
1394 };
1395 self.data.cjs_imports.resolved.insert(call.span.lo, usage);
1396 }
1397
1398 if matches!(
1399 &node.prop,
1400 MemberProp::Ident(..)
1401 | MemberProp::PrivateName(..)
1402 | MemberProp::Computed(ComputedPropName {
1403 expr: Expr::Lit(Lit::Str(_)),
1404 ..
1405 })
1406 ) && let Expr::Ident(ident) = &*node.obj
1407 {
1408 ident.visit_with(self);
1411 } else {
1412 node.visit_children_with(self);
1413 }
1414 }
1415
1416 fn visit_expr(&mut self, node: &Expr) {
1417 if let Expr::Ident(i) = node
1420 && let Some(module_path) = self.namespace_imports_to_specifier.get(&i.to_id())
1421 {
1422 self.data.full_star_imports.insert(module_path.clone());
1423 }
1424 node.visit_children_with(self);
1425 }
1426
1427 fn visit_pat(&mut self, pat: &Pat) {
1428 if let Pat::Ident(i) = pat {
1429 self.register_assignment_scope(i.to_id());
1430 if let Some(module_path) = self.namespace_imports_to_specifier.get(&i.to_id()) {
1431 self.data.full_star_imports.insert(module_path.clone());
1432 }
1433 }
1434 pat.visit_children_with(self);
1435 }
1436
1437 fn visit_simple_assign_target(&mut self, node: &SimpleAssignTarget) {
1438 if let SimpleAssignTarget::Ident(i) = node {
1439 self.register_assignment_scope(i.to_id());
1440 if let Some(module_path) = self.namespace_imports_to_specifier.get(&i.to_id()) {
1441 self.data.full_star_imports.insert(module_path.clone());
1442 }
1443 }
1444 node.visit_children_with(self);
1445 }
1446
1447 fn visit_ident(&mut self, node: &Ident) {
1448 let id = node.to_id();
1449 if let Some((esm_reference_index, _)) = self.data.get_binding(&id) {
1450 let usage = self
1452 .program_decl_usage
1453 .import_usages
1454 .entry(esm_reference_index)
1455 .or_default();
1456 if let Some(top_level) = self.state.cur_top_level_decl_name() {
1457 usage.add_usage(top_level);
1458 } else {
1459 usage.make_side_effects();
1460 }
1461 } else {
1462 if !is_unresolved(node, self.unresolved_mark) {
1464 if let Some(top_level) = self.state.cur_top_level_decl_name() {
1465 if &id != top_level {
1466 self.program_decl_usage
1467 .decl_usages
1468 .entry(id)
1469 .or_default()
1470 .add_usage(top_level);
1471 }
1472 } else {
1473 self.program_decl_usage
1474 .decl_usages
1475 .entry(id)
1476 .or_default()
1477 .make_side_effects();
1478 }
1479 }
1480 }
1481 }
1482
1483 fn visit_fn_expr(&mut self, node: &FnExpr) {
1484 if let Some(ident) = &node.ident {
1485 self.register_assignment_scope(ident.to_id());
1486 }
1487 node.visit_children_with(self);
1488 }
1489
1490 fn visit_fn_decl(&mut self, node: &FnDecl) {
1491 self.enter_top_level_decl(&node.ident, |this| {
1492 node.visit_children_with(this);
1493 });
1494 }
1495
1496 fn visit_decl(&mut self, node: &Decl) {
1497 match node {
1498 Decl::Class(c) => {
1499 self.register_assignment_scope(c.ident.to_id());
1500 }
1501 Decl::Fn(f) => {
1502 self.register_assignment_scope(f.ident.to_id());
1503 }
1504 Decl::Using(v) => {
1505 let ids: Vec<Id> = find_pat_ids(&v.decls);
1506 for id in ids {
1507 self.register_assignment_scope(id);
1508 }
1509 }
1510 Decl::Var(v) => {
1511 let ids: Vec<Id> = find_pat_ids(&v.decls);
1512 for id in ids {
1513 self.register_assignment_scope(id);
1514 }
1515 }
1516 Decl::TsInterface(_) | Decl::TsTypeAlias(_) | Decl::TsEnum(_) | Decl::TsModule(_) => {}
1517 }
1518 node.visit_children_with(self);
1519 }
1520
1521 fn visit_var_declarator(&mut self, node: &VarDeclarator) {
1522 self.record_require_usage_var(node);
1523 node.visit_children_with(self);
1524 }
1525
1526 fn visit_expr_stmt(&mut self, node: &ExprStmt) {
1527 if let Some(call) = as_require_call(&node.expr, self.unresolved_mark) {
1529 self.data
1530 .cjs_imports
1531 .resolved
1532 .insert(call.span.lo, ExportUsage::Evaluation);
1533 }
1534 node.visit_children_with(self);
1535 }
1536
1537 fn visit_update_expr(&mut self, node: &UpdateExpr) {
1538 if let Some(key) = node.arg.as_ident() {
1539 self.register_assignment_scope(key.to_id());
1541 }
1542 node.visit_children_with(self);
1543 }
1544}
1545
1546fn parse_directives(
1549 comments: &dyn Comments,
1550 value: Option<&ExprOrSpread>,
1551) -> Option<ImportAttributes> {
1552 let value = value?;
1553 let leading_comments = comments.get_leading(value.span_lo())?;
1554
1555 let mut ignore = None;
1556 let mut optional = None;
1557 let mut export_names = None;
1558 let mut chunking_type = None;
1559
1560 for comment in leading_comments.iter() {
1562 if let Some((directive, val)) = comment.text.trim().split_once(':') {
1563 let val = val.trim();
1564 match directive.trim() {
1565 "webpackIgnore" | "turbopackIgnore" => match val {
1566 "true" => ignore = Some(true),
1567 "false" => ignore = Some(false),
1568 _ => {}
1569 },
1570 "turbopackOptional" => match val {
1571 "true" => optional = Some(true),
1572 "false" => optional = Some(false),
1573 _ => {}
1574 },
1575 "webpackExports" | "turbopackExports" => {
1576 export_names = Some(parse_export_names(val));
1577 }
1578 "turbopackChunkingType" => {
1579 chunking_type = parse_chunking_type_annotation(value.span(), val);
1580 }
1581 _ => {} }
1583 }
1584 }
1585
1586 if ignore.is_some() || optional.is_some() || export_names.is_some() || chunking_type.is_some() {
1588 Some(ImportAttributes {
1589 ignore: ignore.unwrap_or(false),
1590 optional: optional.unwrap_or(false),
1591 export_names,
1592 chunking_type,
1593 })
1594 } else {
1595 None
1596 }
1597}
1598
1599fn parse_export_names(val: &str) -> SmallVec<[RcStr; 1]> {
1605 let val = val.trim();
1606
1607 if let Ok(names) = serde_json::from_str::<Vec<String>>(val) {
1609 return names.into_iter().map(|s| s.into()).collect();
1610 }
1611
1612 if let Ok(name) = serde_json::from_str::<String>(val) {
1614 return SmallVec::from_buf([name.into()]);
1615 }
1616
1617 if !val.is_empty() {
1619 return SmallVec::from_buf([val.into()]);
1620 }
1621
1622 SmallVec::new()
1623}
1624
1625fn parse_with(with: Option<&ObjectLit>) -> Option<ImportedSymbol> {
1626 find_turbopack_part_id_in_asserts(with?).map(|v| match v {
1627 PartId::Internal(index, true) => ImportedSymbol::PartEvaluation(index),
1628 PartId::Internal(index, false) => ImportedSymbol::Part(index),
1629 PartId::ModuleEvaluation => ImportedSymbol::ModuleEvaluation,
1630 PartId::Export(e) => ImportedSymbol::Symbol(e.as_str().into()),
1631 PartId::Exports => ImportedSymbol::Exports,
1632 })
1633}
1634
1635fn get_import_symbol_from_import(specifier: &ImportSpecifier) -> ImportedSymbol {
1636 match specifier {
1637 ImportSpecifier::Named(ImportNamedSpecifier {
1638 local, imported, ..
1639 }) => ImportedSymbol::Symbol(match imported {
1640 Some(imported) => imported.atom().into_owned(),
1641 _ => local.sym.clone(),
1642 }),
1643 ImportSpecifier::Default(..) => ImportedSymbol::Symbol(atom!("default")),
1644 ImportSpecifier::Namespace(..) => ImportedSymbol::Exports,
1645 }
1646}
1647
1648fn get_import_symbol_from_export(specifier: &ExportSpecifier) -> ImportedSymbol {
1649 match specifier {
1650 ExportSpecifier::Named(ExportNamedSpecifier { orig, .. }) => {
1651 ImportedSymbol::Symbol(orig.atom().into_owned())
1652 }
1653 ExportSpecifier::Default(..) => ImportedSymbol::Symbol(atom!("default")),
1654 ExportSpecifier::Namespace(..) => ImportedSymbol::Exports,
1655 }
1656}
1657
1658fn as_require_call(expr: &Expr, unresolved_mark: Mark) -> Option<&CallExpr> {
1660 let Expr::Call(call) = unparen(expr) else {
1661 return None;
1662 };
1663 let Callee::Expr(callee) = &call.callee else {
1664 return None;
1665 };
1666 let Expr::Ident(f) = &**callee else {
1667 return None;
1668 };
1669 if !is_global(f, "require", unresolved_mark) {
1670 return None;
1671 }
1672 let [arg] = &call.args[..] else {
1673 return None;
1674 };
1675 if arg.spread.is_some() || !matches!(unparen(&arg.expr), Expr::Lit(Lit::Str(_))) {
1676 return None;
1677 }
1678 Some(call)
1679}
1680
1681#[cfg(test)]
1682mod tests {
1683 use swc_core::{atoms::Atom, common::DUMMY_SP};
1684
1685 use super::*;
1686
1687 fn str_lit(s: &str) -> Box<Expr> {
1689 Box::new(Expr::Lit(Lit::Str(Str {
1690 span: DUMMY_SP,
1691 value: Atom::from(s).into(),
1692 raw: None,
1693 })))
1694 }
1695
1696 fn ident_key(s: &str) -> PropName {
1698 PropName::Ident(IdentName {
1699 span: DUMMY_SP,
1700 sym: Atom::from(s),
1701 })
1702 }
1703
1704 fn kv_prop(key: PropName, value: Box<Expr>) -> PropOrSpread {
1706 PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { key, value })))
1707 }
1708
1709 #[test]
1710 fn test_parse_turbopack_loader_annotation() {
1711 let with = ObjectLit {
1713 span: DUMMY_SP,
1714 props: vec![kv_prop(ident_key("turbopackLoader"), str_lit("raw-loader"))],
1715 };
1716
1717 let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1718 assert!(annotations.has_turbopack_loader());
1719
1720 let loader = annotations.turbopack_loader().unwrap();
1721 assert_eq!(loader.loader.as_str(), "raw-loader");
1722 assert!(loader.options.is_empty());
1723 }
1724
1725 #[test]
1726 fn test_parse_turbopack_loader_with_options() {
1727 let with = ObjectLit {
1729 span: DUMMY_SP,
1730 props: vec![
1731 kv_prop(ident_key("turbopackLoader"), str_lit("my-loader")),
1732 kv_prop(
1733 ident_key("turbopackLoaderOptions"),
1734 str_lit(r#"{"flag":true}"#),
1735 ),
1736 ],
1737 };
1738
1739 let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1740 assert!(annotations.has_turbopack_loader());
1741
1742 let loader = annotations.turbopack_loader().unwrap();
1743 assert_eq!(loader.loader.as_str(), "my-loader");
1744 assert_eq!(loader.options["flag"], serde_json::Value::Bool(true));
1745 }
1746
1747 #[test]
1748 fn test_parse_without_turbopack_loader() {
1749 let with = ObjectLit {
1751 span: DUMMY_SP,
1752 props: vec![kv_prop(ident_key("type"), str_lit("json"))],
1753 };
1754
1755 let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1756 assert!(!annotations.has_turbopack_loader());
1757 assert!(annotations.module_type().is_some());
1758 }
1759
1760 #[test]
1761 fn test_parse_empty_with() {
1762 let annotations = ImportAnnotations::parse(None);
1763 assert!(annotations.is_none());
1764 }
1765}