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