Skip to main content

turbopack_ecmascript/analyzer/
imports.rs

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 rustc_hash::{FxHashMap, FxHashSet};
10use smallvec::SmallVec;
11use swc_core::{
12    atoms::Wtf8Atom,
13    common::{BytePos, GLOBALS, Mark, Span, Spanned, SyntaxContext, comments::Comments},
14    ecma::{
15        ast::*,
16        atoms::{Atom, atom},
17        utils::{IsDirective, find_pat_ids},
18        visit::{Visit, VisitWith},
19    },
20};
21use turbo_frozenmap::FrozenMap;
22use turbo_rcstr::{RcStr, rcstr};
23use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc};
24use turbopack_core::{loader::WebpackLoaderItem, resolve::ImportUsage};
25
26use super::{JsValue, ModuleValue, top_level_await::has_top_level_await};
27use crate::{
28    SpecifiedModuleType,
29    analyzer::{
30        Bump, ConstantValue, ObjectPart,
31        graph::{AssignmentScope, AssignmentScopes, EvalContext},
32        is_unresolved, is_unresolved_id,
33    },
34    magic_identifier::{MAGIC_IDENTIFIER_DEFAULT_EXPORT, MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM},
35    references::{
36        esm::{EsmAssetReference, EsmExport, Liveness},
37        util::{SpecifiedChunkingType, parse_chunking_type_annotation},
38    },
39    tree_shake::{PartId, find_turbopack_part_id_in_asserts},
40};
41
42#[turbo_tasks::value]
43#[derive(Default, Debug, Clone, Hash)]
44pub struct ImportAnnotations {
45    // TODO store this in more structured way
46    #[turbo_tasks(trace_ignore)]
47    #[bincode(with_serde)]
48    map: BTreeMap<Wtf8Atom, Wtf8Atom>,
49    /// Parsed turbopack loader configuration from import attributes.
50    /// e.g. `import "file" with { turbopackLoader: "raw-loader" }`
51    #[turbo_tasks(trace_ignore)]
52    #[bincode(with_serde)]
53    turbopack_loader: Option<WebpackLoaderItem>,
54    turbopack_rename_as: Option<RcStr>,
55    turbopack_module_type: Option<RcStr>,
56    chunking_type: Option<SpecifiedChunkingType>,
57}
58
59/// Enables a specified transition for the annotated import
60static ANNOTATION_TRANSITION: LazyLock<Wtf8Atom> =
61    LazyLock::new(|| crate::annotations::ANNOTATION_TRANSITION.into());
62
63/// Changes the type of the resolved module (only "json" is supported currently)
64static ATTRIBUTE_MODULE_TYPE: LazyLock<Wtf8Atom> = LazyLock::new(|| atom!("type").into());
65
66impl ImportAnnotations {
67    pub fn parse(with: Option<&ObjectLit>) -> Option<ImportAnnotations> {
68        let with = with?;
69
70        let mut map = BTreeMap::new();
71        let mut turbopack_loader_name: Option<RcStr> = None;
72        let mut turbopack_loader_options: serde_json::Map<String, serde_json::Value> =
73            serde_json::Map::new();
74        let mut turbopack_rename_as: Option<RcStr> = None;
75        let mut turbopack_module_type: Option<RcStr> = None;
76        let mut chunking_type: Option<SpecifiedChunkingType> = None;
77
78        for prop in &with.props {
79            let Some(kv) = prop.as_prop().and_then(|p| p.as_key_value()) else {
80                continue;
81            };
82
83            let key_str = match &kv.key {
84                PropName::Ident(ident) => Cow::Borrowed(ident.sym.as_str()),
85                PropName::Str(str) => str.value.to_string_lossy(),
86                _ => continue,
87            };
88
89            // All turbopack* keys are extracted as string values (per TC39 import attributes spec)
90            match &*key_str {
91                "turbopackLoader" => {
92                    if let Some(Lit::Str(s)) = kv.value.as_lit() {
93                        turbopack_loader_name =
94                            Some(RcStr::from(s.value.to_string_lossy().into_owned()));
95                    }
96                }
97                "turbopackLoaderOptions" => {
98                    if let Some(Lit::Str(s)) = kv.value.as_lit() {
99                        let json_str = s.value.to_string_lossy();
100                        if let Ok(serde_json::Value::Object(map)) = serde_json::from_str(&json_str)
101                        {
102                            turbopack_loader_options = map;
103                        }
104                    }
105                }
106                "turbopackAs" => {
107                    if let Some(Lit::Str(s)) = kv.value.as_lit() {
108                        turbopack_rename_as =
109                            Some(RcStr::from(s.value.to_string_lossy().into_owned()));
110                    }
111                }
112                "turbopackModuleType" => {
113                    if let Some(Lit::Str(s)) = kv.value.as_lit() {
114                        turbopack_module_type =
115                            Some(RcStr::from(s.value.to_string_lossy().into_owned()));
116                    }
117                }
118                "turbopack-chunking-type" => {
119                    if let Some(Lit::Str(s)) = kv.value.as_lit() {
120                        chunking_type = parse_chunking_type_annotation(
121                            kv.value.span(),
122                            &s.value.to_string_lossy(),
123                        );
124                    }
125                }
126                _ => {
127                    // For all other keys, only accept string values (per spec)
128                    if let Some(Lit::Str(str)) = kv.value.as_lit() {
129                        let key: Wtf8Atom = match &kv.key {
130                            PropName::Ident(ident) => ident.sym.clone().into(),
131                            PropName::Str(s) => s.value.clone(),
132                            _ => continue,
133                        };
134                        map.insert(key, str.value.clone());
135                    }
136                }
137            }
138        }
139
140        let turbopack_loader = turbopack_loader_name.map(|name| WebpackLoaderItem {
141            loader: name,
142            options: turbopack_loader_options,
143        });
144
145        if !map.is_empty()
146            || turbopack_loader.is_some()
147            || turbopack_rename_as.is_some()
148            || turbopack_module_type.is_some()
149            || chunking_type.is_some()
150        {
151            Some(ImportAnnotations {
152                map,
153                turbopack_loader,
154                turbopack_rename_as,
155                turbopack_module_type,
156                chunking_type,
157            })
158        } else {
159            None
160        }
161    }
162
163    pub fn parse_dynamic(with: &JsValue<'_>) -> Option<ImportAnnotations> {
164        let mut map = BTreeMap::new();
165
166        let JsValue::Object { parts, .. } = with else {
167            return None;
168        };
169
170        for part in parts.iter() {
171            let ObjectPart::KeyValue(key, value) = part else {
172                continue;
173            };
174            let (
175                JsValue::Constant(ConstantValue::Str(key)),
176                JsValue::Constant(ConstantValue::Str(value)),
177            ) = (key, value)
178            else {
179                continue;
180            };
181
182            map.insert(
183                key.as_atom().into_owned().into(),
184                value.as_atom().into_owned().into(),
185            );
186        }
187
188        if !map.is_empty() {
189            Some(ImportAnnotations {
190                map,
191                turbopack_loader: None,
192                turbopack_rename_as: None,
193                turbopack_module_type: None,
194                chunking_type: None,
195            })
196        } else {
197            None
198        }
199    }
200
201    /// Returns the content on the transition annotation
202    pub fn transition(&self) -> Option<Cow<'_, str>> {
203        self.get(&ANNOTATION_TRANSITION)
204            .map(|v| v.to_string_lossy())
205    }
206
207    /// Returns the content on the chunking-type annotation
208    pub fn chunking_type(&self) -> Option<SpecifiedChunkingType> {
209        self.chunking_type
210    }
211
212    /// Returns the content on the type attribute
213    pub fn module_type(&self) -> Option<&Wtf8Atom> {
214        self.get(&ATTRIBUTE_MODULE_TYPE)
215    }
216
217    /// Returns the turbopackLoader item, if present
218    pub fn turbopack_loader(&self) -> Option<&WebpackLoaderItem> {
219        self.turbopack_loader.as_ref()
220    }
221
222    /// Returns the turbopackAs rename configuration, if present
223    pub fn turbopack_rename_as(&self) -> Option<&RcStr> {
224        self.turbopack_rename_as.as_ref()
225    }
226
227    /// Returns the turbopackModuleType override, if present
228    pub fn turbopack_module_type(&self) -> Option<&RcStr> {
229        self.turbopack_module_type.as_ref()
230    }
231
232    /// Returns true if a turbopack loader is configured
233    pub fn has_turbopack_loader(&self) -> bool {
234        self.turbopack_loader.is_some()
235    }
236
237    pub fn get(&self, key: &Wtf8Atom) -> Option<&Wtf8Atom> {
238        self.map.get(key)
239    }
240}
241
242impl Display for ImportAnnotations {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        let mut it = self.map.iter();
245        if let Some((k, v)) = it.next() {
246            write!(f, "{{ {}: {}", k.to_string_lossy(), v.to_string_lossy())?
247        } else {
248            return f.write_str("{}");
249        };
250        for (k, v) in it {
251            write!(f, ", {}: {}", k.to_string_lossy(), v.to_string_lossy())?
252        }
253        f.write_str(" }")
254    }
255}
256
257#[derive(Clone, Debug)]
258pub enum DeclUsage {
259    SideEffects,
260    Bindings(FxHashSet<Id>),
261}
262impl Default for DeclUsage {
263    fn default() -> Self {
264        DeclUsage::Bindings(Default::default())
265    }
266}
267impl DeclUsage {
268    fn add_usage(&mut self, user: &Id) {
269        match self {
270            Self::Bindings(set) => {
271                set.insert(user.clone());
272            }
273            Self::SideEffects => {}
274        }
275    }
276    fn make_side_effects(&mut self) {
277        *self = Self::SideEffects;
278    }
279}
280
281#[derive(Default, Debug)]
282pub(crate) struct ProgramDeclUsage {
283    // ident -> immediate usage (top level decl)
284    pub(crate) decl_usages: FxHashMap<Id, DeclUsage>,
285    // import -> immediate usage (top level decl)
286    pub(crate) import_usages: FxHashMap<usize, DeclUsage>,
287    // export name -> top level decl
288    pub(crate) exports: FxHashMap<RcStr, Id>,
289}
290impl ProgramDeclUsage {
291    fn compute_import_usage(&self) -> FxHashMap<usize, ImportUsage> {
292        let mut import_usage =
293            FxHashMap::with_capacity_and_hasher(self.import_usages.len(), Default::default());
294        for (reference, usage) in &self.import_usages {
295            // TODO make this more efficient, i.e. cache the result?
296            if let DeclUsage::Bindings(ids) = usage {
297                // compute transitive closure of `ids` over `top_level_mappings`
298                let mut visited = ids.clone();
299                let mut stack = ids.iter().collect::<Vec<_>>();
300                let mut has_global_usage = false;
301                while let Some(id) = stack.pop() {
302                    match self.decl_usages.get(id) {
303                        Some(DeclUsage::SideEffects) => {
304                            has_global_usage = true;
305                            break;
306                        }
307                        Some(DeclUsage::Bindings(callers)) => {
308                            for caller in callers {
309                                if visited.insert(caller.clone()) {
310                                    stack.push(caller);
311                                }
312                            }
313                        }
314                        _ => {}
315                    }
316                }
317
318                // Collect all `visited` declarations which are exported
319                import_usage.insert(
320                    *reference,
321                    if has_global_usage {
322                        ImportUsage::TopLevel
323                    } else {
324                        ImportUsage::Exports(
325                            self.exports
326                                .iter()
327                                .filter(|(_, id)| visited.contains(*id))
328                                .map(|(exported, _)| exported.clone())
329                                .collect(),
330                        )
331                    },
332                );
333            }
334        }
335        import_usage
336    }
337}
338
339/// A version of [crate::references::esm::export::EsmExport] with usize instead of the module
340/// reference Vc, and missing the liveness fields.
341#[derive(Debug)]
342pub enum Export {
343    /// A local binding that is exported (export { a } or export const a = 1)
344    ///
345    /// Fields: (local_name, is_fake_esm)
346    LocalBinding(RcStr, bool),
347    /// An imported binding that is exported (export { a as b } from "...")
348    ///
349    /// Fields: (module_reference, name, is_fake_esm)
350    ImportedBinding(usize, RcStr, bool),
351    /// An imported namespace that is exported (export * from "...")
352    ImportedNamespace(usize),
353    /// An error occurred while resolving the export
354    Error,
355}
356
357/// The storage for all kinds of imports.
358///
359/// Note that when it's initialized by calling `analyze`, it only contains ESM
360/// import/exports.
361#[derive(Default, Debug)]
362pub(crate) struct ImportMap {
363    /// Map from identifier to (index in references, exported symbol)
364    imports: FxIndexMap<Id, (usize, Atom)>,
365
366    /// Map from identifier to index in references
367    namespace_imports: FxIndexMap<Id, usize>,
368
369    /// Map from exported name to the export
370    exports: BTreeMap<RcStr, Export>,
371
372    /// List of namespace re-exports
373    reexport_namespaces: Vec<usize>,
374
375    /// Ordered list of imported symbols
376    references: FxIndexSet<ImportMapReference>,
377
378    /// True, when the module has an import declaration. imports.is_empty() is not sufficient
379    /// because of side-effect only imports without imported bindings.
380    has_imports: bool,
381
382    /// True, when the module has an export declaration. exports.is_empty() is not sufficient
383    /// because of `export {}`
384    has_exports: bool,
385
386    /// True if the module is an ESM module due to top-level await.
387    has_top_level_await: bool,
388
389    /// True if the module has "use strict"
390    pub(crate) strict: bool,
391
392    /// Locations of [webpack-style "magic comments"][magic] that override import behaviors.
393    ///
394    /// Most commonly, these are `/* webpackIgnore: true */` comments. See [ImportAttributes] for
395    /// full details.
396    ///
397    /// [magic]: https://webpack.js.org/api/module-methods/#magic-comments
398    attributes: FxHashMap<BytePos, ImportAttributes>,
399
400    /// The module specifiers of star imports that are accessed dynamically and should be imported
401    /// as a whole.
402    full_star_imports: FxHashSet<Wtf8Atom>,
403
404    /// Map from export binding id to the scopes where it's assigned. This is used to determine
405    /// whether an export is live or not.
406    pub(super) assignment_scopes: FxHashMap<Id, AssignmentScopes>,
407
408    pub(crate) import_usage: FxHashMap<usize, ImportUsage>,
409
410    /// Map from exported name to local binding id (includes the syntax context).
411    pub(crate) exports_ids: FxHashMap<RcStr, Id>,
412}
413
414/// Represents a collection of [webpack-style "magic comments"][magic] that override import
415/// behaviors.
416///
417/// [magic]: https://webpack.js.org/api/module-methods/#magic-comments
418#[derive(Debug)]
419pub struct ImportAttributes {
420    /// Should we ignore this import expression when bundling? If so, the import expression will be
421    /// left as-is in Turbopack's output.
422    ///
423    /// This is set by using either a `webpackIgnore` or `turbopackIgnore` comment.
424    ///
425    /// Example:
426    /// ```js
427    /// const a = import(/* webpackIgnore: true */ "a");
428    /// const b = import(/* turbopackIgnore: true */ "b");
429    /// ```
430    pub ignore: bool,
431    /// Should resolution errors be suppressed? If so, resolution errors will be completely
432    /// ignored (no error or warning emitted at build time).
433    ///
434    /// This is set by using a `turbopackOptional` comment.
435    ///
436    /// Example:
437    /// ```js
438    /// const a = import(/* turbopackOptional: true */ "a");
439    /// ```
440    pub optional: bool,
441    /// Which exports are used from a dynamic import. When set, enables tree-shaking for the
442    /// dynamically imported module by only including the specified exports.
443    ///
444    /// This is set by using either a `webpackExports` or `turbopackExports` comment.
445    /// `None` means no directive was found (all exports assumed used).
446    /// `Some([])` means empty list (only side effects).
447    /// `Some([name, ...])` means specific named exports are used.
448    ///
449    /// Example:
450    /// ```js
451    /// const { a } = await import(/* webpackExports: ["a"] */ "module");
452    /// const { b } = await import(/* turbopackExports: "b" */ "module");
453    /// ```
454    pub export_names: Option<SmallVec<[RcStr; 1]>>,
455    /// Whether to use a specific chunking type for this import.
456    //
457    /// This is set by using a or `turbopackChunkingType` comment.
458    ///
459    /// Example:
460    /// ```js
461    /// const a = require(/* turbopackChunkingType: parallel */ "a");
462    /// ```
463    pub chunking_type: Option<SpecifiedChunkingType>,
464}
465
466impl ImportAttributes {
467    pub const fn empty() -> Self {
468        ImportAttributes {
469            ignore: false,
470            optional: false,
471            export_names: None,
472            chunking_type: None,
473        }
474    }
475
476    pub fn empty_ref() -> &'static Self {
477        // use `Self::empty` here as `Default::default` isn't const
478        static DEFAULT_VALUE: ImportAttributes = ImportAttributes::empty();
479        &DEFAULT_VALUE
480    }
481}
482
483impl Default for ImportAttributes {
484    fn default() -> Self {
485        ImportAttributes::empty()
486    }
487}
488
489impl Default for &ImportAttributes {
490    fn default() -> Self {
491        ImportAttributes::empty_ref()
492    }
493}
494
495#[derive(Debug, Clone, PartialEq, Eq, Hash)]
496pub(crate) enum ImportedSymbol {
497    ModuleEvaluation,
498    Symbol(Atom),
499    Exports,
500    Part(u32),
501    PartEvaluation(u32),
502}
503
504#[derive(Debug, Clone, PartialEq, Eq, Hash)]
505pub(crate) struct ImportMapReference {
506    pub module_path: Wtf8Atom,
507    pub imported_symbol: ImportedSymbol,
508    pub annotations: Option<Arc<ImportAnnotations>>,
509    pub span: Span,
510}
511
512impl ImportMap {
513    pub fn is_esm(&self, specified_type: SpecifiedModuleType) -> bool {
514        if self.has_exports {
515            return true;
516        }
517
518        match specified_type {
519            SpecifiedModuleType::Automatic => {
520                self.has_exports || self.has_imports || self.has_top_level_await
521            }
522            SpecifiedModuleType::CommonJs => false,
523            SpecifiedModuleType::EcmaScript => true,
524        }
525    }
526
527    pub fn get_import<'a>(&self, arena: &'a Bump, id: &Id) -> Option<JsValue<'a>> {
528        if let Some((i, i_sym)) = self.imports.get(id) {
529            let r = &self.references[*i];
530            return Some(JsValue::member(
531                arena,
532                JsValue::Module(ModuleValue {
533                    module: r.module_path.clone(),
534                    annotations: r.annotations.clone(),
535                }),
536                i_sym.clone().into(),
537            ));
538        }
539        if let Some(i) = self.namespace_imports.get(id) {
540            let r = &self.references[*i];
541            return Some(JsValue::Module(ModuleValue {
542                module: r.module_path.clone(),
543                annotations: r.annotations.clone(),
544            }));
545        }
546        None
547    }
548
549    pub fn get_attributes(&self, span: Span) -> &ImportAttributes {
550        self.attributes.get(&span.lo).unwrap_or_default()
551    }
552
553    pub fn get_binding(&self, id: &Id) -> Option<(usize, Option<&Atom>)> {
554        if let Some((i, i_sym)) = self.imports.get(id) {
555            return Some((*i, Some(i_sym)));
556        }
557        if let Some(i) = self.namespace_imports.get(id) {
558            return Some((*i, None));
559        }
560        None
561    }
562
563    pub fn references(&self) -> impl ExactSizeIterator<Item = &ImportMapReference> {
564        self.references.iter()
565    }
566
567    pub fn reexports_reference_idxs(&self) -> impl Iterator<Item = usize> {
568        self.exports
569            .values()
570            .filter_map(|value| match value {
571                Export::ImportedBinding(i, ..) | Export::ImportedNamespace(i) => Some(*i),
572                Export::LocalBinding(..) | Export::Error => None,
573            })
574            .chain(self.reexport_namespaces.iter().copied())
575    }
576
577    pub fn as_esm_exports(
578        &self,
579        import_references: &[ResolvedVc<EsmAssetReference>],
580        eval_context: &EvalContext,
581    ) -> Result<FrozenMap<RcStr, EsmExport>> {
582        Ok(FrozenMap::from(
583            self.exports
584                .iter()
585                .map(|(name, value)| {
586                    let value = match value {
587                        Export::LocalBinding(local, is_fake_esm) => EsmExport::LocalBinding(
588                            local.clone(),
589                            if *is_fake_esm {
590                                // it is likely that these are not always actually mutable.
591                                Liveness::Mutable
592                            } else {
593                                eval_context.imports.get_export_ident_liveness(
594                                    self.exports_ids.get(name).cloned().with_context(|| {
595                                        format!("Exported binding {name} not found in exports_ids")
596                                    })?,
597                                    eval_context.unresolved_mark,
598                                )
599                            },
600                        ),
601                        Export::ImportedBinding(i, name, is_fake_esm) => {
602                            EsmExport::ImportedBinding(
603                                ResolvedVc::upcast(import_references[*i]),
604                                name.clone(),
605                                *is_fake_esm,
606                            )
607                        }
608                        Export::ImportedNamespace(i) => {
609                            EsmExport::ImportedNamespace(ResolvedVc::upcast(import_references[*i]))
610                        }
611                        Export::Error => EsmExport::Error,
612                    };
613                    Ok((name.clone(), value))
614                })
615                .collect::<Result<Vec<_>>>()?,
616        ))
617    }
618
619    pub fn reexport_namespaces(&self) -> impl ExactSizeIterator<Item = usize> {
620        self.reexport_namespaces.iter().copied()
621    }
622
623    /// Returns the liveness of a given export identifier. An export is live if it might change
624    /// values after module evaluation.
625    pub fn get_export_ident_liveness(&self, id: Id, unresolved_mark: Mark) -> Liveness {
626        if let Some(assignment_scopes) = self.assignment_scopes.get(&id) {
627            // If all assignments are in module scope, the export is not live.
628            if *assignment_scopes != AssignmentScopes::AllInModuleEvalScope {
629                Liveness::Live
630            } else {
631                Liveness::Constant
632            }
633        } else {
634            // If we haven't computed a value for it, that means it might be
635            // - A free variable or
636            // - an imported variable
637            // In those cases, we just assume that the value is live since we don't know anything
638            debug_assert!(
639                self.imports.contains_key(&id)
640                    || self.namespace_imports.contains_key(&id)
641                    || !GLOBALS.is_set()
642                    || is_unresolved_id(&id, unresolved_mark),
643                "export ident {id:?} without an assignment scope should be a free variable or an \
644                 imported variable"
645            );
646
647            Liveness::Live
648        }
649    }
650
651    /// Analyze ES import
652    pub(super) fn analyze(
653        unresolved_mark: Mark,
654        m: &Program,
655        comments: Option<&dyn Comments>,
656    ) -> Self {
657        let mut data = ImportMap::default();
658        let mut analyzer = Analyzer {
659            unresolved_mark,
660            data: &mut data,
661            comments,
662            namespace_imports_to_specifier: FxIndexMap::default(),
663            state: Default::default(),
664            program_decl_usage: Default::default(),
665        };
666
667        // A prepass to detect imports to be able to rewrite import+export pairs to true reexports
668        if let Program::Module(m) = m {
669            for stmt in &m.body {
670                match stmt {
671                    ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => {
672                        if import.type_only {
673                            continue;
674                        }
675                        analyzer.data.has_imports = true;
676                        let annotations = ImportAnnotations::parse(import.with.as_deref());
677                        let internal_symbol = parse_with(import.with.as_deref());
678                        if internal_symbol.is_none() {
679                            analyzer.ensure_reference(
680                                import.span,
681                                import.src.value.clone(),
682                                ImportedSymbol::ModuleEvaluation,
683                                annotations.clone(),
684                            );
685                        }
686
687                        for s in &import.specifiers {
688                            if s.is_type_only() {
689                                continue;
690                            }
691                            let symbol = internal_symbol
692                                .clone()
693                                .unwrap_or_else(|| get_import_symbol_from_import(s));
694                            let i = analyzer.ensure_reference(
695                                import.span,
696                                import.src.value.clone(),
697                                symbol,
698                                annotations.clone(),
699                            );
700
701                            let (local, orig_sym) = match s {
702                                ImportSpecifier::Namespace(s) => {
703                                    analyzer
704                                        .namespace_imports_to_specifier
705                                        .insert(s.local.to_id(), import.src.value.clone());
706                                    analyzer.data.namespace_imports.insert(s.local.to_id(), i);
707                                    continue;
708                                }
709                                ImportSpecifier::Default(s) => (s.local.to_id(), atom!("default")),
710                                ImportSpecifier::Named(s) => match &s.imported {
711                                    Some(imported) => {
712                                        (s.local.to_id(), imported.atom().into_owned())
713                                    }
714                                    _ => (s.local.to_id(), s.local.sym.clone()),
715                                },
716                            };
717                            analyzer.data.imports.insert(local, (i, orig_sym));
718                        }
719                        if import.specifiers.is_empty()
720                            && let Some(internal_symbol) = internal_symbol
721                        {
722                            analyzer.ensure_reference(
723                                import.span,
724                                import.src.value.clone(),
725                                internal_symbol,
726                                annotations,
727                            );
728                        }
729                    }
730                    // We need to call ensure_reference in this loop to ensure that the reference
731                    // order of all hoisted imports (be it import or reexport) is correct.
732                    ModuleItem::ModuleDecl(ModuleDecl::ExportAll(export)) => {
733                        if export.type_only {
734                            continue;
735                        }
736                        let annotations = ImportAnnotations::parse(export.with.as_deref());
737                        analyzer.ensure_reference(
738                            export.span,
739                            export.src.value.clone(),
740                            ImportedSymbol::ModuleEvaluation,
741                            annotations.clone(),
742                        );
743                    }
744                    ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
745                        if export.type_only {
746                            continue;
747                        }
748                        if let Some(ref src) = export.src {
749                            let annotations = ImportAnnotations::parse(export.with.as_deref());
750                            let internal_symbol = parse_with(export.with.as_deref());
751                            if internal_symbol.is_none() || export.specifiers.is_empty() {
752                                analyzer.ensure_reference(
753                                    export.span,
754                                    src.value.clone(),
755                                    ImportedSymbol::ModuleEvaluation,
756                                    annotations.clone(),
757                                );
758                            }
759                        }
760                    }
761                    _ => (),
762                }
763            }
764        }
765
766        m.visit_with(&mut analyzer);
767
768        data.import_usage = analyzer.program_decl_usage.compute_import_usage();
769
770        data
771    }
772
773    pub(crate) fn should_import_all(&self, esm_reference_index: usize) -> bool {
774        let r = &self.references[esm_reference_index];
775
776        self.full_star_imports.contains(&r.module_path)
777    }
778}
779
780mod analyzer_state {
781    use swc_core::ecma::ast::{Id, Ident};
782
783    use super::Analyzer;
784
785    #[derive(Default)]
786    pub(super) struct AnalyzerState {
787        is_in_fn: bool,
788        cur_top_level_decl_name: Option<Id>,
789    }
790
791    impl AnalyzerState {
792        /// Returns the identifier of the current top level declaration.
793        pub(super) fn cur_top_level_decl_name(&self) -> &Option<Id> {
794            &self.cur_top_level_decl_name
795        }
796
797        /// Returns whether the current context is inside a function.
798        pub(super) fn is_in_fn(&self) -> bool {
799            self.is_in_fn
800        }
801    }
802
803    impl Analyzer<'_> {
804        /// Runs `visitor` with the current top level declaration identifier
805        pub(super) fn enter_top_level_decl<T>(
806            &mut self,
807            name: &Ident,
808            visitor: impl FnOnce(&mut Self) -> T,
809        ) -> T {
810            let is_top_level_fn = self.state.cur_top_level_decl_name.is_none();
811            if is_top_level_fn {
812                self.state.cur_top_level_decl_name = Some(name.to_id());
813            }
814            let result = visitor(self);
815            if is_top_level_fn {
816                self.state.cur_top_level_decl_name = None;
817            }
818            result
819        }
820
821        /// Runs `visitor` with the right is_in_fn value
822        pub(super) fn enter_fn<T>(&mut self, visitor: impl FnOnce(&mut Self) -> T) -> T {
823            let old_is_in_fn = self.state.is_in_fn;
824            self.state.is_in_fn = true;
825            let result = visitor(self);
826            self.state.is_in_fn = old_is_in_fn;
827            result
828        }
829    }
830}
831
832struct Analyzer<'a> {
833    unresolved_mark: Mark,
834    data: &'a mut ImportMap,
835    comments: Option<&'a dyn Comments>,
836    /// Map from local identifier of namespace imports to module path, used temporarily during
837    /// analysis to detect dynamic accesses to namespace imports.
838    namespace_imports_to_specifier: FxIndexMap<Id, Wtf8Atom>,
839
840    program_decl_usage: ProgramDeclUsage,
841
842    state: analyzer_state::AnalyzerState,
843}
844
845impl Analyzer<'_> {
846    fn ensure_reference(
847        &mut self,
848        span: Span,
849        module_path: Wtf8Atom,
850        imported_symbol: ImportedSymbol,
851        annotations: Option<ImportAnnotations>,
852    ) -> usize {
853        let r = ImportMapReference {
854            module_path,
855            imported_symbol,
856            span,
857            annotations: annotations.map(Arc::new),
858        };
859        if let Some(i) = self.data.references.get_index_of(&r) {
860            i
861        } else {
862            let i = self.data.references.len();
863            self.data.references.insert(r);
864            i
865        }
866    }
867
868    fn register_assignment_scope(&mut self, id: Id) {
869        let scope = if self.state.is_in_fn() {
870            AssignmentScope::Function
871        } else {
872            AssignmentScope::ModuleEval
873        };
874
875        match self.data.assignment_scopes.entry(id) {
876            Entry::Occupied(mut e) => {
877                *e.get_mut() = e.get().merge(scope);
878            }
879            Entry::Vacant(e) => {
880                e.insert(AssignmentScopes::new(scope));
881            }
882        }
883    }
884}
885
886impl Visit for Analyzer<'_> {
887    fn visit_import_decl(&mut self, _: &ImportDecl) {
888        // We already handled import above. Skip as the Idents in here confuse the analysis
889    }
890
891    fn visit_export_all(&mut self, export: &ExportAll) {
892        if export.type_only {
893            return;
894        }
895
896        let annotations = ImportAnnotations::parse(export.with.as_deref());
897
898        let symbol = parse_with(export.with.as_deref());
899        let i = self.ensure_reference(
900            export.span,
901            export.src.value.clone(),
902            symbol.unwrap_or(ImportedSymbol::Exports),
903            annotations,
904        );
905        self.data.reexport_namespaces.push(i);
906        self.data.has_exports = true;
907        export.visit_children_with(self);
908    }
909
910    fn visit_named_export(&mut self, export: &NamedExport) {
911        if export.type_only {
912            return;
913        }
914
915        self.data.has_exports = true;
916
917        if let Some(ref src) = export.src {
918            let annotations = ImportAnnotations::parse(export.with.as_deref());
919            let internal_symbol = parse_with(export.with.as_deref());
920
921            for spec in export.specifiers.iter() {
922                let symbol = internal_symbol
923                    .clone()
924                    .unwrap_or_else(|| get_import_symbol_from_export(spec));
925
926                let i = self.ensure_reference(
927                    export.span,
928                    src.value.clone(),
929                    symbol,
930                    annotations.clone(),
931                );
932
933                match spec {
934                    ExportSpecifier::Namespace(n) => {
935                        self.data.exports.insert(
936                            RcStr::from(n.name.atom().as_str()),
937                            Export::ImportedNamespace(i),
938                        );
939                    }
940                    ExportSpecifier::Default(d) => {
941                        self.data.exports.insert(
942                            RcStr::from(d.exported.sym.as_str()),
943                            Export::ImportedBinding(i, rcstr!("default"), false),
944                        );
945                    }
946                    ExportSpecifier::Named(n) => {
947                        self.data.exports.insert(
948                            RcStr::from(n.exported.as_ref().unwrap_or(&n.orig).atom().as_str()),
949                            Export::ImportedBinding(i, RcStr::from(n.orig.atom().as_str()), false),
950                        );
951                    }
952                }
953            }
954        } else {
955            for spec in export.specifiers.iter() {
956                match spec {
957                    ExportSpecifier::Namespace(_) => {
958                        unreachable!(
959                            "ExportNamespaceSpecifier will not happen in combination with src == \
960                             None"
961                        );
962                    }
963                    ExportSpecifier::Default(_) => {
964                        unreachable!(
965                            "ExportDefaultSpecifier will not happen in combination with src == \
966                             None"
967                        );
968                    }
969                    ExportSpecifier::Named(ExportNamedSpecifier {
970                        orig,
971                        exported,
972                        is_type_only,
973                        ..
974                    }) => {
975                        if *is_type_only {
976                            continue;
977                        }
978
979                        // We create mutable exports for fake ESMs generated by module splitting
980                        let is_fake_esm = export
981                            .with
982                            .as_deref()
983                            .map(find_turbopack_part_id_in_asserts)
984                            .is_some();
985                        let export = {
986                            let imported_binding = if let ModuleExportName::Ident(ident) = orig {
987                                self.data.get_binding(&ident.to_id())
988                            } else {
989                                None
990                            };
991                            if let Some((index, export)) = imported_binding {
992                                // This is a export of an imported binding. Rewrite to a true
993                                // reexport.
994                                if let Some(export) = export {
995                                    Export::ImportedBinding(
996                                        index,
997                                        RcStr::from(export.as_str()),
998                                        is_fake_esm,
999                                    )
1000                                } else {
1001                                    Export::ImportedNamespace(index)
1002                                }
1003                            } else {
1004                                Export::LocalBinding(RcStr::from(orig.atom().as_str()), is_fake_esm)
1005                            }
1006                        };
1007                        self.data.exports.insert(
1008                            RcStr::from(exported.as_ref().unwrap_or(orig).atom().as_str()),
1009                            export,
1010                        );
1011                    }
1012                }
1013            }
1014            export.visit_children_with(self);
1015        }
1016    }
1017
1018    fn visit_export_decl(&mut self, n: &ExportDecl) {
1019        self.data.has_exports = true;
1020        match &n.decl {
1021            Decl::Class(n) => {
1022                let name = RcStr::from(n.ident.sym.as_str());
1023                self.data
1024                    .exports
1025                    .insert(name.clone(), Export::LocalBinding(name.clone(), false));
1026                self.data.exports_ids.insert(name.clone(), n.ident.to_id());
1027                self.program_decl_usage
1028                    .exports
1029                    .insert(name, n.ident.to_id());
1030            }
1031            Decl::Fn(n) => {
1032                let name = RcStr::from(n.ident.sym.as_str());
1033                self.data
1034                    .exports
1035                    .insert(name.clone(), Export::LocalBinding(name.clone(), false));
1036                self.data.exports_ids.insert(name.clone(), n.ident.to_id());
1037                self.program_decl_usage
1038                    .exports
1039                    .insert(name, n.ident.to_id());
1040            }
1041            Decl::Var(..) => {
1042                let ids: Vec<Id> = find_pat_ids(&n.decl);
1043                for id in ids {
1044                    let name = RcStr::from(id.0.as_str());
1045                    self.data
1046                        .exports
1047                        .insert(name.clone(), Export::LocalBinding(name.clone(), false));
1048                    self.data.exports_ids.insert(name.clone(), id.clone());
1049                    self.program_decl_usage.exports.insert(name, id);
1050                }
1051            }
1052            Decl::Using(_) => {
1053                // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export#:~:text=You%20cannot%20use%20export%20on%20a%20using%20or%20await%20using%20declaration
1054                unreachable!("using declarations can not be exported");
1055            }
1056            Decl::TsInterface(_) | Decl::TsTypeAlias(_) | Decl::TsEnum(_) | Decl::TsModule(_) => {
1057                // ignore typescript for code generation
1058            }
1059        }
1060
1061        n.visit_children_with(self);
1062    }
1063
1064    fn visit_export_default_decl(&mut self, n: &ExportDefaultDecl) {
1065        self.data.has_exports = true;
1066
1067        let id = match &n.decl {
1068            DefaultDecl::Class(ClassExpr { ident, .. }) | DefaultDecl::Fn(FnExpr { ident, .. }) => {
1069                // Mirror what `EsmModuleItem::code_generation` does, these are live bindings if the
1070                // class/function has an identifier.
1071                ident.as_ref().map_or_else(
1072                    || {
1073                        (
1074                            MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM.clone(),
1075                            SyntaxContext::empty(),
1076                        )
1077                    },
1078                    |ident| ident.to_id(),
1079                )
1080            }
1081            DefaultDecl::TsInterfaceDecl(_) => {
1082                // not matching, might happen due to eventual consistency
1083                (
1084                    MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM.clone(),
1085                    SyntaxContext::empty(),
1086                )
1087            }
1088        };
1089
1090        self.register_assignment_scope(id.clone());
1091        self.data.exports.insert(
1092            rcstr!("default"),
1093            Export::LocalBinding(RcStr::from(id.0.as_str()), false),
1094        );
1095        self.data.exports_ids.insert(rcstr!("default"), id.clone());
1096        self.program_decl_usage
1097            .exports
1098            .insert(rcstr!("default"), id);
1099        n.visit_children_with(self);
1100    }
1101
1102    fn visit_export_default_expr(&mut self, n: &ExportDefaultExpr) {
1103        self.data.has_exports = true;
1104
1105        let default_id = (
1106            MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM.clone(),
1107            SyntaxContext::empty(),
1108        );
1109
1110        self.data.exports.insert(
1111            rcstr!("default"),
1112            Export::LocalBinding(MAGIC_IDENTIFIER_DEFAULT_EXPORT.clone(), false),
1113        );
1114        self.data
1115            .exports_ids
1116            .insert(rcstr!("default"), default_id.clone());
1117
1118        self.register_assignment_scope(default_id);
1119        n.visit_children_with(self);
1120    }
1121
1122    fn visit_export_named_specifier(&mut self, n: &ExportNamedSpecifier) {
1123        self.data.has_exports = true;
1124
1125        let ModuleExportName::Ident(local) = &n.orig else {
1126            unreachable!("exporting a string should be impossible")
1127        };
1128        let exported = RcStr::from(n.exported.as_ref().unwrap_or(&n.orig).atom().as_str());
1129        self.data
1130            .exports_ids
1131            .insert(exported.clone(), local.to_id());
1132        self.program_decl_usage
1133            .exports
1134            .insert(exported, local.to_id());
1135        n.visit_children_with(self);
1136    }
1137
1138    fn visit_export_default_specifier(&mut self, n: &ExportDefaultSpecifier) {
1139        self.data.has_exports = true;
1140
1141        self.data
1142            .exports_ids
1143            .insert(rcstr!("default"), n.exported.to_id());
1144        n.visit_children_with(self);
1145    }
1146
1147    fn visit_program(&mut self, m: &Program) {
1148        self.data.has_top_level_await = has_top_level_await(m).is_some();
1149        self.data.strict = match m {
1150            Program::Module(module) => module
1151                .body
1152                .iter()
1153                .take_while(|s| s.directive_continue())
1154                .any(IsDirective::is_use_strict),
1155            Program::Script(script) => script
1156                .body
1157                .iter()
1158                .take_while(|s| s.directive_continue())
1159                .any(IsDirective::is_use_strict),
1160        };
1161
1162        m.visit_children_with(self);
1163    }
1164
1165    /// check if import or require contains magic comments
1166    ///
1167    /// We are checking for the following cases:
1168    /// - import(/* webpackIgnore: true */ "a")
1169    /// - require(/* webpackIgnore: true */ "a")
1170    /// - import(/* turbopackOptional: true */ "a")
1171    /// - require(/* turbopackOptional: true */ "a")
1172    ///
1173    /// We can do this by checking if any of the comment spans are between the
1174    /// callee and the first argument.
1175    //
1176    // potentially support more webpack magic comments in the future:
1177    // https://webpack.js.org/api/module-methods/#magic-comments
1178    fn visit_call_expr(&mut self, n: &CallExpr) {
1179        if let Some(comments) = self.comments {
1180            let callee_span = match &n.callee {
1181                Callee::Import(Import { span, .. }) => Some(*span),
1182                Callee::Expr(e) => Some(e.span()),
1183                _ => None,
1184            };
1185
1186            if let Some(callee_span) = callee_span
1187                && let Some(attributes) = parse_directives(comments, n.args.first())
1188            {
1189                self.data.attributes.insert(callee_span.lo, attributes);
1190            }
1191        }
1192
1193        n.visit_children_with(self);
1194    }
1195
1196    fn visit_new_expr(&mut self, n: &NewExpr) {
1197        if let Some(comments) = self.comments {
1198            let callee_span = match &*n.callee {
1199                Expr::Ident(Ident { sym, .. }) if sym == "Worker" => Some(n.span),
1200                _ => None,
1201            };
1202
1203            if let Some(callee_span) = callee_span
1204                && let Some(attributes) = parse_directives(comments, n.args.iter().flatten().next())
1205            {
1206                self.data.attributes.insert(callee_span.lo, attributes);
1207            }
1208        }
1209
1210        n.visit_children_with(self);
1211    }
1212
1213    fn visit_getter_prop(&mut self, node: &GetterProp) {
1214        self.enter_fn(|this| {
1215            node.visit_children_with(this);
1216        });
1217    }
1218    fn visit_setter_prop(&mut self, node: &SetterProp) {
1219        self.enter_fn(|this| {
1220            node.visit_children_with(this);
1221        });
1222    }
1223    fn visit_function(&mut self, node: &Function) {
1224        self.enter_fn(|this| {
1225            node.visit_children_with(this);
1226        });
1227    }
1228    fn visit_constructor(&mut self, node: &Constructor) {
1229        self.enter_fn(|this| {
1230            node.visit_children_with(this);
1231        });
1232    }
1233    fn visit_arrow_expr(&mut self, node: &ArrowExpr) {
1234        self.enter_fn(|this| {
1235            node.visit_children_with(this);
1236        });
1237    }
1238
1239    fn visit_member_expr(&mut self, node: &MemberExpr) {
1240        if matches!(
1241            &node.prop,
1242            MemberProp::Ident(..) | MemberProp::PrivateName(..)
1243        ) && let Expr::Ident(ident) = &*node.obj
1244        {
1245            // Intentionally skipping over visit_expr(node.obj) here so that it doesn't get added to
1246            // full_star_imports below in visit_expr.
1247            ident.visit_with(self);
1248        } else {
1249            node.visit_children_with(self);
1250        }
1251    }
1252
1253    fn visit_expr(&mut self, node: &Expr) {
1254        // Careful about adding anything here, visit_member_expr might skip over this method for
1255        // some Expr::Ident-s.
1256        if let Expr::Ident(i) = node
1257            && let Some(module_path) = self.namespace_imports_to_specifier.get(&i.to_id())
1258        {
1259            self.data.full_star_imports.insert(module_path.clone());
1260        }
1261        node.visit_children_with(self);
1262    }
1263
1264    fn visit_pat(&mut self, pat: &Pat) {
1265        if let Pat::Ident(i) = pat {
1266            self.register_assignment_scope(i.to_id());
1267            if let Some(module_path) = self.namespace_imports_to_specifier.get(&i.to_id()) {
1268                self.data.full_star_imports.insert(module_path.clone());
1269            }
1270        }
1271        pat.visit_children_with(self);
1272    }
1273
1274    fn visit_simple_assign_target(&mut self, node: &SimpleAssignTarget) {
1275        if let SimpleAssignTarget::Ident(i) = node {
1276            self.register_assignment_scope(i.to_id());
1277            if let Some(module_path) = self.namespace_imports_to_specifier.get(&i.to_id()) {
1278                self.data.full_star_imports.insert(module_path.clone());
1279            }
1280        }
1281        node.visit_children_with(self);
1282    }
1283
1284    fn visit_ident(&mut self, node: &Ident) {
1285        let id = node.to_id();
1286        if let Some((esm_reference_index, _)) = self.data.get_binding(&id) {
1287            // An import binding
1288            let usage = self
1289                .program_decl_usage
1290                .import_usages
1291                .entry(esm_reference_index)
1292                .or_default();
1293            if let Some(top_level) = self.state.cur_top_level_decl_name() {
1294                usage.add_usage(top_level);
1295            } else {
1296                usage.make_side_effects();
1297            }
1298        } else {
1299            // A regular variable
1300            if !is_unresolved(node, self.unresolved_mark) {
1301                if let Some(top_level) = self.state.cur_top_level_decl_name() {
1302                    if &id != top_level {
1303                        self.program_decl_usage
1304                            .decl_usages
1305                            .entry(id)
1306                            .or_default()
1307                            .add_usage(top_level);
1308                    }
1309                } else {
1310                    self.program_decl_usage
1311                        .decl_usages
1312                        .entry(id)
1313                        .or_default()
1314                        .make_side_effects();
1315                }
1316            }
1317        }
1318    }
1319
1320    fn visit_fn_expr(&mut self, node: &FnExpr) {
1321        if let Some(ident) = &node.ident {
1322            self.register_assignment_scope(ident.to_id());
1323        }
1324        node.visit_children_with(self);
1325    }
1326
1327    fn visit_fn_decl(&mut self, node: &FnDecl) {
1328        self.enter_top_level_decl(&node.ident, |this| {
1329            node.visit_children_with(this);
1330        });
1331    }
1332
1333    fn visit_decl(&mut self, node: &Decl) {
1334        match node {
1335            Decl::Class(c) => {
1336                self.register_assignment_scope(c.ident.to_id());
1337            }
1338            Decl::Fn(f) => {
1339                self.register_assignment_scope(f.ident.to_id());
1340            }
1341            Decl::Using(v) => {
1342                let ids: Vec<Id> = find_pat_ids(&v.decls);
1343                for id in ids {
1344                    self.register_assignment_scope(id);
1345                }
1346            }
1347            Decl::Var(v) => {
1348                let ids: Vec<Id> = find_pat_ids(&v.decls);
1349                for id in ids {
1350                    self.register_assignment_scope(id);
1351                }
1352            }
1353            Decl::TsInterface(_) | Decl::TsTypeAlias(_) | Decl::TsEnum(_) | Decl::TsModule(_) => {}
1354        }
1355        node.visit_children_with(self);
1356    }
1357
1358    fn visit_update_expr(&mut self, node: &UpdateExpr) {
1359        if let Some(key) = node.arg.as_ident() {
1360            // node.arg can also be a member expression
1361            self.register_assignment_scope(key.to_id());
1362        }
1363        node.visit_children_with(self);
1364    }
1365}
1366
1367/// Parse magic comment directives from the leading comments of a call argument.
1368/// Returns (ignore, optional) directives if any are found.
1369fn parse_directives(
1370    comments: &dyn Comments,
1371    value: Option<&ExprOrSpread>,
1372) -> Option<ImportAttributes> {
1373    let value = value?;
1374    let leading_comments = comments.get_leading(value.span_lo())?;
1375
1376    let mut ignore = None;
1377    let mut optional = None;
1378    let mut export_names = None;
1379    let mut chunking_type = None;
1380
1381    // Process all comments, last one wins for each directive type
1382    for comment in leading_comments.iter() {
1383        if let Some((directive, val)) = comment.text.trim().split_once(':') {
1384            let val = val.trim();
1385            match directive.trim() {
1386                "webpackIgnore" | "turbopackIgnore" => match val {
1387                    "true" => ignore = Some(true),
1388                    "false" => ignore = Some(false),
1389                    _ => {}
1390                },
1391                "turbopackOptional" => match val {
1392                    "true" => optional = Some(true),
1393                    "false" => optional = Some(false),
1394                    _ => {}
1395                },
1396                "webpackExports" | "turbopackExports" => {
1397                    export_names = Some(parse_export_names(val));
1398                }
1399                "turbopackChunkingType" => {
1400                    chunking_type = parse_chunking_type_annotation(value.span(), val);
1401                }
1402                _ => {} // ignore anything else
1403            }
1404        }
1405    }
1406
1407    // Return Some only if at least one directive was found
1408    if ignore.is_some() || optional.is_some() || export_names.is_some() || chunking_type.is_some() {
1409        Some(ImportAttributes {
1410            ignore: ignore.unwrap_or(false),
1411            optional: optional.unwrap_or(false),
1412            export_names,
1413            chunking_type,
1414        })
1415    } else {
1416        None
1417    }
1418}
1419
1420/// Parse export names from a `webpackExports` or `turbopackExports` comment value.
1421///
1422/// Supports two formats:
1423/// - Single string: `"name"` → `["name"]`
1424/// - JSON array: `["name1", "name2"]` → `["name1", "name2"]`
1425fn parse_export_names(val: &str) -> SmallVec<[RcStr; 1]> {
1426    let val = val.trim();
1427
1428    // Try parsing as JSON array of strings
1429    if let Ok(names) = serde_json::from_str::<Vec<String>>(val) {
1430        return names.into_iter().map(|s| s.into()).collect();
1431    }
1432
1433    // Try parsing as a single JSON string
1434    if let Ok(name) = serde_json::from_str::<String>(val) {
1435        return SmallVec::from_buf([name.into()]);
1436    }
1437
1438    // Bare identifier (no quotes)
1439    if !val.is_empty() {
1440        return SmallVec::from_buf([val.into()]);
1441    }
1442
1443    SmallVec::new()
1444}
1445
1446fn parse_with(with: Option<&ObjectLit>) -> Option<ImportedSymbol> {
1447    find_turbopack_part_id_in_asserts(with?).map(|v| match v {
1448        PartId::Internal(index, true) => ImportedSymbol::PartEvaluation(index),
1449        PartId::Internal(index, false) => ImportedSymbol::Part(index),
1450        PartId::ModuleEvaluation => ImportedSymbol::ModuleEvaluation,
1451        PartId::Export(e) => ImportedSymbol::Symbol(e.as_str().into()),
1452        PartId::Exports => ImportedSymbol::Exports,
1453    })
1454}
1455
1456fn get_import_symbol_from_import(specifier: &ImportSpecifier) -> ImportedSymbol {
1457    match specifier {
1458        ImportSpecifier::Named(ImportNamedSpecifier {
1459            local, imported, ..
1460        }) => ImportedSymbol::Symbol(match imported {
1461            Some(imported) => imported.atom().into_owned(),
1462            _ => local.sym.clone(),
1463        }),
1464        ImportSpecifier::Default(..) => ImportedSymbol::Symbol(atom!("default")),
1465        ImportSpecifier::Namespace(..) => ImportedSymbol::Exports,
1466    }
1467}
1468
1469fn get_import_symbol_from_export(specifier: &ExportSpecifier) -> ImportedSymbol {
1470    match specifier {
1471        ExportSpecifier::Named(ExportNamedSpecifier { orig, .. }) => {
1472            ImportedSymbol::Symbol(orig.atom().into_owned())
1473        }
1474        ExportSpecifier::Default(..) => ImportedSymbol::Symbol(atom!("default")),
1475        ExportSpecifier::Namespace(..) => ImportedSymbol::Exports,
1476    }
1477}
1478
1479#[cfg(test)]
1480mod tests {
1481    use swc_core::{atoms::Atom, common::DUMMY_SP};
1482
1483    use super::*;
1484
1485    /// Helper to create a string literal expression
1486    fn str_lit(s: &str) -> Box<Expr> {
1487        Box::new(Expr::Lit(Lit::Str(Str {
1488            span: DUMMY_SP,
1489            value: Atom::from(s).into(),
1490            raw: None,
1491        })))
1492    }
1493
1494    /// Helper to create an ident property name
1495    fn ident_key(s: &str) -> PropName {
1496        PropName::Ident(IdentName {
1497            span: DUMMY_SP,
1498            sym: Atom::from(s),
1499        })
1500    }
1501
1502    /// Helper to create a key-value property
1503    fn kv_prop(key: PropName, value: Box<Expr>) -> PropOrSpread {
1504        PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { key, value })))
1505    }
1506
1507    #[test]
1508    fn test_parse_turbopack_loader_annotation() {
1509        // Simulate: with { turbopackLoader: "raw-loader" }
1510        let with = ObjectLit {
1511            span: DUMMY_SP,
1512            props: vec![kv_prop(ident_key("turbopackLoader"), str_lit("raw-loader"))],
1513        };
1514
1515        let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1516        assert!(annotations.has_turbopack_loader());
1517
1518        let loader = annotations.turbopack_loader().unwrap();
1519        assert_eq!(loader.loader.as_str(), "raw-loader");
1520        assert!(loader.options.is_empty());
1521    }
1522
1523    #[test]
1524    fn test_parse_turbopack_loader_with_options() {
1525        // Simulate: with { turbopackLoader: "my-loader", turbopackLoaderOptions: '{"flag":true}' }
1526        let with = ObjectLit {
1527            span: DUMMY_SP,
1528            props: vec![
1529                kv_prop(ident_key("turbopackLoader"), str_lit("my-loader")),
1530                kv_prop(
1531                    ident_key("turbopackLoaderOptions"),
1532                    str_lit(r#"{"flag":true}"#),
1533                ),
1534            ],
1535        };
1536
1537        let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1538        assert!(annotations.has_turbopack_loader());
1539
1540        let loader = annotations.turbopack_loader().unwrap();
1541        assert_eq!(loader.loader.as_str(), "my-loader");
1542        assert_eq!(loader.options["flag"], serde_json::Value::Bool(true));
1543    }
1544
1545    #[test]
1546    fn test_parse_without_turbopack_loader() {
1547        // Simulate: with { type: "json" }
1548        let with = ObjectLit {
1549            span: DUMMY_SP,
1550            props: vec![kv_prop(ident_key("type"), str_lit("json"))],
1551        };
1552
1553        let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1554        assert!(!annotations.has_turbopack_loader());
1555        assert!(annotations.module_type().is_some());
1556    }
1557
1558    #[test]
1559    fn test_parse_empty_with() {
1560        let annotations = ImportAnnotations::parse(None);
1561        assert!(annotations.is_none());
1562    }
1563}