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, 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        ConstantValue, ObjectPart,
31        graph::{AssignmentScope, AssignmentScopes, EvalContext},
32        is_unresolved,
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(&self, id: &Id) -> Option<JsValue> {
528        if let Some((i, i_sym)) = self.imports.get(id) {
529            let r = &self.references[*i];
530            return Some(JsValue::member(
531                Box::new(JsValue::Module(ModuleValue {
532                    module: r.module_path.clone(),
533                    annotations: r.annotations.clone(),
534                })),
535                Box::new(i_sym.clone().into()),
536            ));
537        }
538        if let Some(i) = self.namespace_imports.get(id) {
539            let r = &self.references[*i];
540            return Some(JsValue::Module(ModuleValue {
541                module: r.module_path.clone(),
542                annotations: r.annotations.clone(),
543            }));
544        }
545        None
546    }
547
548    pub fn get_attributes(&self, span: Span) -> &ImportAttributes {
549        self.attributes.get(&span.lo).unwrap_or_default()
550    }
551
552    pub fn get_binding(&self, id: &Id) -> Option<(usize, Option<&Atom>)> {
553        if let Some((i, i_sym)) = self.imports.get(id) {
554            return Some((*i, Some(i_sym)));
555        }
556        if let Some(i) = self.namespace_imports.get(id) {
557            return Some((*i, None));
558        }
559        None
560    }
561
562    pub fn references(&self) -> impl ExactSizeIterator<Item = &ImportMapReference> {
563        self.references.iter()
564    }
565
566    pub fn reexports_reference_idxs(&self) -> impl Iterator<Item = usize> {
567        self.exports
568            .values()
569            .filter_map(|value| match value {
570                Export::ImportedBinding(i, ..) | Export::ImportedNamespace(i) => Some(*i),
571                Export::LocalBinding(..) | Export::Error => None,
572            })
573            .chain(self.reexport_namespaces.iter().copied())
574    }
575
576    pub fn as_esm_exports(
577        &self,
578        import_references: &[ResolvedVc<EsmAssetReference>],
579        eval_context: &EvalContext,
580    ) -> Result<FrozenMap<RcStr, EsmExport>> {
581        Ok(FrozenMap::from(
582            self.exports
583                .iter()
584                .map(|(name, value)| {
585                    let value = match value {
586                        Export::LocalBinding(local, is_fake_esm) => EsmExport::LocalBinding(
587                            local.clone(),
588                            if *is_fake_esm {
589                                // it is likely that these are not always actually mutable.
590                                Liveness::Mutable
591                            } else {
592                                eval_context.imports.get_export_ident_liveness(
593                                    self.exports_ids.get(name).cloned().with_context(|| {
594                                        format!("Exported binding {name} not found in exports_ids")
595                                    })?,
596                                )
597                            },
598                        ),
599                        Export::ImportedBinding(i, name, is_fake_esm) => {
600                            EsmExport::ImportedBinding(
601                                ResolvedVc::upcast(import_references[*i]),
602                                name.clone(),
603                                *is_fake_esm,
604                            )
605                        }
606                        Export::ImportedNamespace(i) => {
607                            EsmExport::ImportedNamespace(ResolvedVc::upcast(import_references[*i]))
608                        }
609                        Export::Error => EsmExport::Error,
610                    };
611                    Ok((name.clone(), value))
612                })
613                .collect::<Result<Vec<_>>>()?,
614        ))
615    }
616
617    pub fn reexport_namespaces(&self) -> impl ExactSizeIterator<Item = usize> {
618        self.reexport_namespaces.iter().copied()
619    }
620
621    /// Returns the liveness of a given export identifier. An export is live if it might change
622    /// values after module evaluation.
623    pub fn get_export_ident_liveness(&self, id: Id) -> Liveness {
624        if let Some(assignment_scopes) = self.assignment_scopes.get(&id) {
625            // If all assignments are in module scope, the export is not live.
626            if *assignment_scopes != AssignmentScopes::AllInModuleEvalScope {
627                Liveness::Live
628            } else {
629                Liveness::Constant
630            }
631        } else {
632            // If we haven't computed a value for it, that means it might be
633            // - A free variable or
634            // - an imported variable
635            // In those cases, we just assume that the value is live since we don't know anything
636            Liveness::Live
637        }
638    }
639
640    /// Analyze ES import
641    pub(super) fn analyze(
642        unresolved_mark: Mark,
643        m: &Program,
644        comments: Option<&dyn Comments>,
645    ) -> Self {
646        let mut data = ImportMap::default();
647        let mut analyzer = Analyzer {
648            unresolved_mark,
649            data: &mut data,
650            comments,
651            namespace_imports_to_specifier: FxIndexMap::default(),
652            state: Default::default(),
653            program_decl_usage: Default::default(),
654        };
655
656        // A prepass to detect imports to be able to rewrite import+export pairs to true reexports
657        if let Program::Module(m) = m {
658            for stmt in &m.body {
659                match stmt {
660                    ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => {
661                        if import.type_only {
662                            continue;
663                        }
664                        analyzer.data.has_imports = true;
665                        let annotations = ImportAnnotations::parse(import.with.as_deref());
666                        let internal_symbol = parse_with(import.with.as_deref());
667                        if internal_symbol.is_none() {
668                            analyzer.ensure_reference(
669                                import.span,
670                                import.src.value.clone(),
671                                ImportedSymbol::ModuleEvaluation,
672                                annotations.clone(),
673                            );
674                        }
675
676                        for s in &import.specifiers {
677                            if s.is_type_only() {
678                                continue;
679                            }
680                            let symbol = internal_symbol
681                                .clone()
682                                .unwrap_or_else(|| get_import_symbol_from_import(s));
683                            let i = analyzer.ensure_reference(
684                                import.span,
685                                import.src.value.clone(),
686                                symbol,
687                                annotations.clone(),
688                            );
689
690                            let (local, orig_sym) = match s {
691                                ImportSpecifier::Namespace(s) => {
692                                    analyzer
693                                        .namespace_imports_to_specifier
694                                        .insert(s.local.to_id(), import.src.value.clone());
695                                    analyzer.data.namespace_imports.insert(s.local.to_id(), i);
696                                    continue;
697                                }
698                                ImportSpecifier::Default(s) => (s.local.to_id(), atom!("default")),
699                                ImportSpecifier::Named(s) => match &s.imported {
700                                    Some(imported) => {
701                                        (s.local.to_id(), imported.atom().into_owned())
702                                    }
703                                    _ => (s.local.to_id(), s.local.sym.clone()),
704                                },
705                            };
706                            analyzer.data.imports.insert(local, (i, orig_sym));
707                        }
708                        if import.specifiers.is_empty()
709                            && let Some(internal_symbol) = internal_symbol
710                        {
711                            analyzer.ensure_reference(
712                                import.span,
713                                import.src.value.clone(),
714                                internal_symbol,
715                                annotations,
716                            );
717                        }
718                    }
719                    // We need to call ensure_reference in this loop to ensure that the reference
720                    // order of all hoisted imports (be it import or reexport) is correct.
721                    ModuleItem::ModuleDecl(ModuleDecl::ExportAll(export)) => {
722                        if export.type_only {
723                            continue;
724                        }
725                        let annotations = ImportAnnotations::parse(export.with.as_deref());
726                        analyzer.ensure_reference(
727                            export.span,
728                            export.src.value.clone(),
729                            ImportedSymbol::ModuleEvaluation,
730                            annotations.clone(),
731                        );
732                    }
733                    ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => {
734                        if export.type_only {
735                            continue;
736                        }
737                        if let Some(ref src) = export.src {
738                            let annotations = ImportAnnotations::parse(export.with.as_deref());
739                            let internal_symbol = parse_with(export.with.as_deref());
740                            if internal_symbol.is_none() || export.specifiers.is_empty() {
741                                analyzer.ensure_reference(
742                                    export.span,
743                                    src.value.clone(),
744                                    ImportedSymbol::ModuleEvaluation,
745                                    annotations.clone(),
746                                );
747                            }
748                        }
749                    }
750                    _ => (),
751                }
752            }
753        }
754
755        m.visit_with(&mut analyzer);
756
757        data.import_usage = analyzer.program_decl_usage.compute_import_usage();
758
759        data
760    }
761
762    pub(crate) fn should_import_all(&self, esm_reference_index: usize) -> bool {
763        let r = &self.references[esm_reference_index];
764
765        self.full_star_imports.contains(&r.module_path)
766    }
767}
768
769mod analyzer_state {
770    use swc_core::ecma::ast::{Id, Ident};
771
772    use super::Analyzer;
773
774    #[derive(Default)]
775    pub(super) struct AnalyzerState {
776        is_in_fn: bool,
777        cur_top_level_decl_name: Option<Id>,
778    }
779
780    impl AnalyzerState {
781        /// Returns the identifier of the current top level declaration.
782        pub(super) fn cur_top_level_decl_name(&self) -> &Option<Id> {
783            &self.cur_top_level_decl_name
784        }
785
786        /// Returns whether the current context is inside a function.
787        pub(super) fn is_in_fn(&self) -> bool {
788            self.is_in_fn
789        }
790    }
791
792    impl Analyzer<'_> {
793        /// Runs `visitor` with the current top level declaration identifier
794        pub(super) fn enter_top_level_decl<T>(
795            &mut self,
796            name: &Ident,
797            visitor: impl FnOnce(&mut Self) -> T,
798        ) -> T {
799            let is_top_level_fn = self.state.cur_top_level_decl_name.is_none();
800            if is_top_level_fn {
801                self.state.cur_top_level_decl_name = Some(name.to_id());
802            }
803            let result = visitor(self);
804            if is_top_level_fn {
805                self.state.cur_top_level_decl_name = None;
806            }
807            result
808        }
809
810        /// Runs `visitor` with the right is_in_fn value
811        pub(super) fn enter_fn<T>(&mut self, visitor: impl FnOnce(&mut Self) -> T) -> T {
812            let old_is_in_fn = self.state.is_in_fn;
813            self.state.is_in_fn = true;
814            let result = visitor(self);
815            self.state.is_in_fn = old_is_in_fn;
816            result
817        }
818    }
819}
820
821struct Analyzer<'a> {
822    unresolved_mark: Mark,
823    data: &'a mut ImportMap,
824    comments: Option<&'a dyn Comments>,
825    /// Map from local identifier of namespace imports to module path, used temporarily during
826    /// analysis to detect dynamic accesses to namespace imports.
827    namespace_imports_to_specifier: FxIndexMap<Id, Wtf8Atom>,
828
829    program_decl_usage: ProgramDeclUsage,
830
831    state: analyzer_state::AnalyzerState,
832}
833
834impl Analyzer<'_> {
835    fn ensure_reference(
836        &mut self,
837        span: Span,
838        module_path: Wtf8Atom,
839        imported_symbol: ImportedSymbol,
840        annotations: Option<ImportAnnotations>,
841    ) -> usize {
842        let r = ImportMapReference {
843            module_path,
844            imported_symbol,
845            span,
846            annotations: annotations.map(Arc::new),
847        };
848        if let Some(i) = self.data.references.get_index_of(&r) {
849            i
850        } else {
851            let i = self.data.references.len();
852            self.data.references.insert(r);
853            i
854        }
855    }
856
857    fn register_assignment_scope(&mut self, id: Id) {
858        let scope = if self.state.is_in_fn() {
859            AssignmentScope::Function
860        } else {
861            AssignmentScope::ModuleEval
862        };
863
864        match self.data.assignment_scopes.entry(id) {
865            Entry::Occupied(mut e) => {
866                *e.get_mut() = e.get().merge(scope);
867            }
868            Entry::Vacant(e) => {
869                e.insert(AssignmentScopes::new(scope));
870            }
871        }
872    }
873}
874
875impl Visit for Analyzer<'_> {
876    fn visit_import_decl(&mut self, _: &ImportDecl) {
877        // We already handled import above. Skip as the Idents in here confuse the analysis
878    }
879
880    fn visit_export_all(&mut self, export: &ExportAll) {
881        if export.type_only {
882            return;
883        }
884
885        let annotations = ImportAnnotations::parse(export.with.as_deref());
886
887        let symbol = parse_with(export.with.as_deref());
888        let i = self.ensure_reference(
889            export.span,
890            export.src.value.clone(),
891            symbol.unwrap_or(ImportedSymbol::Exports),
892            annotations,
893        );
894        self.data.reexport_namespaces.push(i);
895        self.data.has_exports = true;
896        export.visit_children_with(self);
897    }
898
899    fn visit_named_export(&mut self, export: &NamedExport) {
900        if export.type_only {
901            return;
902        }
903
904        self.data.has_exports = true;
905
906        if let Some(ref src) = export.src {
907            let annotations = ImportAnnotations::parse(export.with.as_deref());
908            let internal_symbol = parse_with(export.with.as_deref());
909
910            for spec in export.specifiers.iter() {
911                let symbol = internal_symbol
912                    .clone()
913                    .unwrap_or_else(|| get_import_symbol_from_export(spec));
914
915                let i = self.ensure_reference(
916                    export.span,
917                    src.value.clone(),
918                    symbol,
919                    annotations.clone(),
920                );
921
922                match spec {
923                    ExportSpecifier::Namespace(n) => {
924                        self.data.exports.insert(
925                            RcStr::from(n.name.atom().as_str()),
926                            Export::ImportedNamespace(i),
927                        );
928                    }
929                    ExportSpecifier::Default(d) => {
930                        self.data.exports.insert(
931                            RcStr::from(d.exported.sym.as_str()),
932                            Export::ImportedBinding(i, rcstr!("default"), false),
933                        );
934                    }
935                    ExportSpecifier::Named(n) => {
936                        self.data.exports.insert(
937                            RcStr::from(n.exported.as_ref().unwrap_or(&n.orig).atom().as_str()),
938                            Export::ImportedBinding(i, RcStr::from(n.orig.atom().as_str()), false),
939                        );
940                    }
941                }
942            }
943        } else {
944            for spec in export.specifiers.iter() {
945                match spec {
946                    ExportSpecifier::Namespace(_) => {
947                        unreachable!(
948                            "ExportNamespaceSpecifier will not happen in combination with src == \
949                             None"
950                        );
951                    }
952                    ExportSpecifier::Default(_) => {
953                        unreachable!(
954                            "ExportDefaultSpecifier will not happen in combination with src == \
955                             None"
956                        );
957                    }
958                    ExportSpecifier::Named(ExportNamedSpecifier {
959                        orig,
960                        exported,
961                        is_type_only,
962                        ..
963                    }) => {
964                        if *is_type_only {
965                            continue;
966                        }
967
968                        // We create mutable exports for fake ESMs generated by module splitting
969                        let is_fake_esm = export
970                            .with
971                            .as_deref()
972                            .map(find_turbopack_part_id_in_asserts)
973                            .is_some();
974                        let export = {
975                            let imported_binding = if let ModuleExportName::Ident(ident) = orig {
976                                self.data.get_binding(&ident.to_id())
977                            } else {
978                                None
979                            };
980                            if let Some((index, export)) = imported_binding {
981                                // This is a export of an imported binding. Rewrite to a true
982                                // reexport.
983                                if let Some(export) = export {
984                                    Export::ImportedBinding(
985                                        index,
986                                        RcStr::from(export.as_str()),
987                                        is_fake_esm,
988                                    )
989                                } else {
990                                    Export::ImportedNamespace(index)
991                                }
992                            } else {
993                                Export::LocalBinding(RcStr::from(orig.atom().as_str()), is_fake_esm)
994                            }
995                        };
996                        self.data.exports.insert(
997                            RcStr::from(exported.as_ref().unwrap_or(orig).atom().as_str()),
998                            export,
999                        );
1000                    }
1001                }
1002            }
1003            export.visit_children_with(self);
1004        }
1005    }
1006
1007    fn visit_export_decl(&mut self, n: &ExportDecl) {
1008        self.data.has_exports = true;
1009        match &n.decl {
1010            Decl::Class(n) => {
1011                let name = RcStr::from(n.ident.sym.as_str());
1012                self.data
1013                    .exports
1014                    .insert(name.clone(), Export::LocalBinding(name.clone(), false));
1015                self.data.exports_ids.insert(name.clone(), n.ident.to_id());
1016                self.program_decl_usage
1017                    .exports
1018                    .insert(name, n.ident.to_id());
1019            }
1020            Decl::Fn(n) => {
1021                let name = RcStr::from(n.ident.sym.as_str());
1022                self.data
1023                    .exports
1024                    .insert(name.clone(), Export::LocalBinding(name.clone(), false));
1025                self.data.exports_ids.insert(name.clone(), n.ident.to_id());
1026                self.program_decl_usage
1027                    .exports
1028                    .insert(name, n.ident.to_id());
1029            }
1030            Decl::Var(..) => {
1031                let ids: Vec<Id> = find_pat_ids(&n.decl);
1032                for id in ids {
1033                    let name = RcStr::from(id.0.as_str());
1034                    self.data
1035                        .exports
1036                        .insert(name.clone(), Export::LocalBinding(name.clone(), false));
1037                    self.data.exports_ids.insert(name.clone(), id.clone());
1038                    self.program_decl_usage.exports.insert(name, id);
1039                }
1040            }
1041            Decl::Using(_) => {
1042                // 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
1043                unreachable!("using declarations can not be exported");
1044            }
1045            Decl::TsInterface(_) | Decl::TsTypeAlias(_) | Decl::TsEnum(_) | Decl::TsModule(_) => {
1046                // ignore typescript for code generation
1047            }
1048        }
1049
1050        n.visit_children_with(self);
1051    }
1052
1053    fn visit_export_default_decl(&mut self, n: &ExportDefaultDecl) {
1054        self.data.has_exports = true;
1055
1056        let id = match &n.decl {
1057            DefaultDecl::Class(ClassExpr { ident, .. }) | DefaultDecl::Fn(FnExpr { ident, .. }) => {
1058                // Mirror what `EsmModuleItem::code_generation` does, these are live bindings if the
1059                // class/function has an identifier.
1060                ident.as_ref().map_or_else(
1061                    || {
1062                        (
1063                            MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM.clone(),
1064                            SyntaxContext::empty(),
1065                        )
1066                    },
1067                    |ident| ident.to_id(),
1068                )
1069            }
1070            DefaultDecl::TsInterfaceDecl(_) => {
1071                // not matching, might happen due to eventual consistency
1072                (
1073                    MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM.clone(),
1074                    SyntaxContext::empty(),
1075                )
1076            }
1077        };
1078
1079        self.data.exports.insert(
1080            rcstr!("default"),
1081            Export::LocalBinding(RcStr::from(id.0.as_str()), false),
1082        );
1083        self.data.exports_ids.insert(rcstr!("default"), id.clone());
1084        self.program_decl_usage
1085            .exports
1086            .insert(rcstr!("default"), id);
1087        n.visit_children_with(self);
1088    }
1089
1090    fn visit_export_default_expr(&mut self, n: &ExportDefaultExpr) {
1091        self.data.has_exports = true;
1092
1093        self.data.exports.insert(
1094            rcstr!("default"),
1095            Export::LocalBinding(MAGIC_IDENTIFIER_DEFAULT_EXPORT.clone(), false),
1096        );
1097        self.data.exports_ids.insert(
1098            rcstr!("default"),
1099            (
1100                // `EsmModuleItem::code_generation` inserts this variable.
1101                MAGIC_IDENTIFIER_DEFAULT_EXPORT_ATOM.clone(),
1102                SyntaxContext::empty(),
1103            ),
1104        );
1105        n.visit_children_with(self);
1106    }
1107
1108    fn visit_export_named_specifier(&mut self, n: &ExportNamedSpecifier) {
1109        self.data.has_exports = true;
1110
1111        let ModuleExportName::Ident(local) = &n.orig else {
1112            unreachable!("exporting a string should be impossible")
1113        };
1114        let exported = RcStr::from(n.exported.as_ref().unwrap_or(&n.orig).atom().as_str());
1115        self.data
1116            .exports_ids
1117            .insert(exported.clone(), local.to_id());
1118        self.program_decl_usage
1119            .exports
1120            .insert(exported, local.to_id());
1121        n.visit_children_with(self);
1122    }
1123
1124    fn visit_export_default_specifier(&mut self, n: &ExportDefaultSpecifier) {
1125        self.data.has_exports = true;
1126
1127        self.data
1128            .exports_ids
1129            .insert(rcstr!("default"), n.exported.to_id());
1130        n.visit_children_with(self);
1131    }
1132
1133    fn visit_program(&mut self, m: &Program) {
1134        self.data.has_top_level_await = has_top_level_await(m).is_some();
1135        self.data.strict = match m {
1136            Program::Module(module) => module
1137                .body
1138                .iter()
1139                .take_while(|s| s.directive_continue())
1140                .any(IsDirective::is_use_strict),
1141            Program::Script(script) => script
1142                .body
1143                .iter()
1144                .take_while(|s| s.directive_continue())
1145                .any(IsDirective::is_use_strict),
1146        };
1147
1148        m.visit_children_with(self);
1149    }
1150
1151    /// check if import or require contains magic comments
1152    ///
1153    /// We are checking for the following cases:
1154    /// - import(/* webpackIgnore: true */ "a")
1155    /// - require(/* webpackIgnore: true */ "a")
1156    /// - import(/* turbopackOptional: true */ "a")
1157    /// - require(/* turbopackOptional: true */ "a")
1158    ///
1159    /// We can do this by checking if any of the comment spans are between the
1160    /// callee and the first argument.
1161    //
1162    // potentially support more webpack magic comments in the future:
1163    // https://webpack.js.org/api/module-methods/#magic-comments
1164    fn visit_call_expr(&mut self, n: &CallExpr) {
1165        if let Some(comments) = self.comments {
1166            let callee_span = match &n.callee {
1167                Callee::Import(Import { span, .. }) => Some(*span),
1168                Callee::Expr(e) => Some(e.span()),
1169                _ => None,
1170            };
1171
1172            if let Some(callee_span) = callee_span
1173                && let Some(attributes) = parse_directives(comments, n.args.first())
1174            {
1175                self.data.attributes.insert(callee_span.lo, attributes);
1176            }
1177        }
1178
1179        n.visit_children_with(self);
1180    }
1181
1182    fn visit_new_expr(&mut self, n: &NewExpr) {
1183        if let Some(comments) = self.comments {
1184            let callee_span = match &*n.callee {
1185                Expr::Ident(Ident { sym, .. }) if sym == "Worker" => Some(n.span),
1186                _ => None,
1187            };
1188
1189            if let Some(callee_span) = callee_span
1190                && let Some(attributes) = parse_directives(comments, n.args.iter().flatten().next())
1191            {
1192                self.data.attributes.insert(callee_span.lo, attributes);
1193            }
1194        }
1195
1196        n.visit_children_with(self);
1197    }
1198
1199    fn visit_getter_prop(&mut self, node: &GetterProp) {
1200        self.enter_fn(|this| {
1201            node.visit_children_with(this);
1202        });
1203    }
1204    fn visit_setter_prop(&mut self, node: &SetterProp) {
1205        self.enter_fn(|this| {
1206            node.visit_children_with(this);
1207        });
1208    }
1209    fn visit_function(&mut self, node: &Function) {
1210        self.enter_fn(|this| {
1211            node.visit_children_with(this);
1212        });
1213    }
1214    fn visit_constructor(&mut self, node: &Constructor) {
1215        self.enter_fn(|this| {
1216            node.visit_children_with(this);
1217        });
1218    }
1219    fn visit_arrow_expr(&mut self, node: &ArrowExpr) {
1220        self.enter_fn(|this| {
1221            node.visit_children_with(this);
1222        });
1223    }
1224
1225    fn visit_member_expr(&mut self, node: &MemberExpr) {
1226        if matches!(
1227            &node.prop,
1228            MemberProp::Ident(..) | MemberProp::PrivateName(..)
1229        ) && let Expr::Ident(ident) = &*node.obj
1230        {
1231            // Intentionally skipping over visit_expr(node.obj) here so that it doesn't get added to
1232            // full_star_imports below in visit_expr.
1233            ident.visit_with(self);
1234        } else {
1235            node.visit_children_with(self);
1236        }
1237    }
1238
1239    fn visit_expr(&mut self, node: &Expr) {
1240        // Careful about adding anything here, visit_member_expr might skip over this method for
1241        // some Expr::Ident-s.
1242        if let Expr::Ident(i) = node
1243            && let Some(module_path) = self.namespace_imports_to_specifier.get(&i.to_id())
1244        {
1245            self.data.full_star_imports.insert(module_path.clone());
1246        }
1247        node.visit_children_with(self);
1248    }
1249
1250    fn visit_pat(&mut self, pat: &Pat) {
1251        if let Pat::Ident(i) = pat {
1252            self.register_assignment_scope(i.to_id());
1253            if let Some(module_path) = self.namespace_imports_to_specifier.get(&i.to_id()) {
1254                self.data.full_star_imports.insert(module_path.clone());
1255            }
1256        }
1257        pat.visit_children_with(self);
1258    }
1259
1260    fn visit_simple_assign_target(&mut self, node: &SimpleAssignTarget) {
1261        if let SimpleAssignTarget::Ident(i) = node {
1262            self.register_assignment_scope(i.to_id());
1263            if let Some(module_path) = self.namespace_imports_to_specifier.get(&i.to_id()) {
1264                self.data.full_star_imports.insert(module_path.clone());
1265            }
1266        }
1267        node.visit_children_with(self);
1268    }
1269
1270    fn visit_ident(&mut self, node: &Ident) {
1271        let id = node.to_id();
1272        if let Some((esm_reference_index, _)) = self.data.get_binding(&id) {
1273            // An import binding
1274            let usage = self
1275                .program_decl_usage
1276                .import_usages
1277                .entry(esm_reference_index)
1278                .or_default();
1279            if let Some(top_level) = self.state.cur_top_level_decl_name() {
1280                usage.add_usage(top_level);
1281            } else {
1282                usage.make_side_effects();
1283            }
1284        } else {
1285            // A regular variable
1286            if !is_unresolved(node, self.unresolved_mark) {
1287                if let Some(top_level) = self.state.cur_top_level_decl_name() {
1288                    if &id != top_level {
1289                        self.program_decl_usage
1290                            .decl_usages
1291                            .entry(id)
1292                            .or_default()
1293                            .add_usage(top_level);
1294                    }
1295                } else {
1296                    self.program_decl_usage
1297                        .decl_usages
1298                        .entry(id)
1299                        .or_default()
1300                        .make_side_effects();
1301                }
1302            }
1303        }
1304    }
1305
1306    fn visit_fn_expr(&mut self, node: &FnExpr) {
1307        if let Some(ident) = &node.ident {
1308            self.register_assignment_scope(ident.to_id());
1309        }
1310        node.visit_children_with(self);
1311    }
1312
1313    fn visit_fn_decl(&mut self, node: &FnDecl) {
1314        self.enter_top_level_decl(&node.ident, |this| {
1315            node.visit_children_with(this);
1316        });
1317    }
1318
1319    fn visit_decl(&mut self, node: &Decl) {
1320        match node {
1321            Decl::Class(c) => {
1322                self.register_assignment_scope(c.ident.to_id());
1323            }
1324            Decl::Fn(f) => {
1325                self.register_assignment_scope(f.ident.to_id());
1326            }
1327            Decl::Using(v) => {
1328                let ids: Vec<Id> = find_pat_ids(&v.decls);
1329                for id in ids {
1330                    self.register_assignment_scope(id);
1331                }
1332            }
1333            Decl::Var(v) => {
1334                let ids: Vec<Id> = find_pat_ids(&v.decls);
1335                for id in ids {
1336                    self.register_assignment_scope(id);
1337                }
1338            }
1339            Decl::TsInterface(_) | Decl::TsTypeAlias(_) | Decl::TsEnum(_) | Decl::TsModule(_) => {}
1340        }
1341        node.visit_children_with(self);
1342    }
1343
1344    fn visit_update_expr(&mut self, node: &UpdateExpr) {
1345        if let Some(key) = node.arg.as_ident() {
1346            // node.arg can also be a member expression
1347            self.register_assignment_scope(key.to_id());
1348        }
1349        node.visit_children_with(self);
1350    }
1351}
1352
1353/// Parse magic comment directives from the leading comments of a call argument.
1354/// Returns (ignore, optional) directives if any are found.
1355fn parse_directives(
1356    comments: &dyn Comments,
1357    value: Option<&ExprOrSpread>,
1358) -> Option<ImportAttributes> {
1359    let value = value?;
1360    let leading_comments = comments.get_leading(value.span_lo())?;
1361
1362    let mut ignore = None;
1363    let mut optional = None;
1364    let mut export_names = None;
1365    let mut chunking_type = None;
1366
1367    // Process all comments, last one wins for each directive type
1368    for comment in leading_comments.iter() {
1369        if let Some((directive, val)) = comment.text.trim().split_once(':') {
1370            let val = val.trim();
1371            match directive.trim() {
1372                "webpackIgnore" | "turbopackIgnore" => match val {
1373                    "true" => ignore = Some(true),
1374                    "false" => ignore = Some(false),
1375                    _ => {}
1376                },
1377                "turbopackOptional" => match val {
1378                    "true" => optional = Some(true),
1379                    "false" => optional = Some(false),
1380                    _ => {}
1381                },
1382                "webpackExports" | "turbopackExports" => {
1383                    export_names = Some(parse_export_names(val));
1384                }
1385                "turbopackChunkingType" => {
1386                    chunking_type = parse_chunking_type_annotation(value.span(), val);
1387                }
1388                _ => {} // ignore anything else
1389            }
1390        }
1391    }
1392
1393    // Return Some only if at least one directive was found
1394    if ignore.is_some() || optional.is_some() || export_names.is_some() || chunking_type.is_some() {
1395        Some(ImportAttributes {
1396            ignore: ignore.unwrap_or(false),
1397            optional: optional.unwrap_or(false),
1398            export_names,
1399            chunking_type,
1400        })
1401    } else {
1402        None
1403    }
1404}
1405
1406/// Parse export names from a `webpackExports` or `turbopackExports` comment value.
1407///
1408/// Supports two formats:
1409/// - Single string: `"name"` → `["name"]`
1410/// - JSON array: `["name1", "name2"]` → `["name1", "name2"]`
1411fn parse_export_names(val: &str) -> SmallVec<[RcStr; 1]> {
1412    let val = val.trim();
1413
1414    // Try parsing as JSON array of strings
1415    if let Ok(names) = serde_json::from_str::<Vec<String>>(val) {
1416        return names.into_iter().map(|s| s.into()).collect();
1417    }
1418
1419    // Try parsing as a single JSON string
1420    if let Ok(name) = serde_json::from_str::<String>(val) {
1421        return SmallVec::from_buf([name.into()]);
1422    }
1423
1424    // Bare identifier (no quotes)
1425    if !val.is_empty() {
1426        return SmallVec::from_buf([val.into()]);
1427    }
1428
1429    SmallVec::new()
1430}
1431
1432fn parse_with(with: Option<&ObjectLit>) -> Option<ImportedSymbol> {
1433    find_turbopack_part_id_in_asserts(with?).map(|v| match v {
1434        PartId::Internal(index, true) => ImportedSymbol::PartEvaluation(index),
1435        PartId::Internal(index, false) => ImportedSymbol::Part(index),
1436        PartId::ModuleEvaluation => ImportedSymbol::ModuleEvaluation,
1437        PartId::Export(e) => ImportedSymbol::Symbol(e.as_str().into()),
1438        PartId::Exports => ImportedSymbol::Exports,
1439    })
1440}
1441
1442fn get_import_symbol_from_import(specifier: &ImportSpecifier) -> ImportedSymbol {
1443    match specifier {
1444        ImportSpecifier::Named(ImportNamedSpecifier {
1445            local, imported, ..
1446        }) => ImportedSymbol::Symbol(match imported {
1447            Some(imported) => imported.atom().into_owned(),
1448            _ => local.sym.clone(),
1449        }),
1450        ImportSpecifier::Default(..) => ImportedSymbol::Symbol(atom!("default")),
1451        ImportSpecifier::Namespace(..) => ImportedSymbol::Exports,
1452    }
1453}
1454
1455fn get_import_symbol_from_export(specifier: &ExportSpecifier) -> ImportedSymbol {
1456    match specifier {
1457        ExportSpecifier::Named(ExportNamedSpecifier { orig, .. }) => {
1458            ImportedSymbol::Symbol(orig.atom().into_owned())
1459        }
1460        ExportSpecifier::Default(..) => ImportedSymbol::Symbol(atom!("default")),
1461        ExportSpecifier::Namespace(..) => ImportedSymbol::Exports,
1462    }
1463}
1464
1465#[cfg(test)]
1466mod tests {
1467    use swc_core::{atoms::Atom, common::DUMMY_SP, ecma::ast::*};
1468
1469    use super::*;
1470
1471    /// Helper to create a string literal expression
1472    fn str_lit(s: &str) -> Box<Expr> {
1473        Box::new(Expr::Lit(Lit::Str(Str {
1474            span: DUMMY_SP,
1475            value: Atom::from(s).into(),
1476            raw: None,
1477        })))
1478    }
1479
1480    /// Helper to create an ident property name
1481    fn ident_key(s: &str) -> PropName {
1482        PropName::Ident(IdentName {
1483            span: DUMMY_SP,
1484            sym: Atom::from(s),
1485        })
1486    }
1487
1488    /// Helper to create a key-value property
1489    fn kv_prop(key: PropName, value: Box<Expr>) -> PropOrSpread {
1490        PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { key, value })))
1491    }
1492
1493    #[test]
1494    fn test_parse_turbopack_loader_annotation() {
1495        // Simulate: with { turbopackLoader: "raw-loader" }
1496        let with = ObjectLit {
1497            span: DUMMY_SP,
1498            props: vec![kv_prop(ident_key("turbopackLoader"), str_lit("raw-loader"))],
1499        };
1500
1501        let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1502        assert!(annotations.has_turbopack_loader());
1503
1504        let loader = annotations.turbopack_loader().unwrap();
1505        assert_eq!(loader.loader.as_str(), "raw-loader");
1506        assert!(loader.options.is_empty());
1507    }
1508
1509    #[test]
1510    fn test_parse_turbopack_loader_with_options() {
1511        // Simulate: with { turbopackLoader: "my-loader", turbopackLoaderOptions: '{"flag":true}' }
1512        let with = ObjectLit {
1513            span: DUMMY_SP,
1514            props: vec![
1515                kv_prop(ident_key("turbopackLoader"), str_lit("my-loader")),
1516                kv_prop(
1517                    ident_key("turbopackLoaderOptions"),
1518                    str_lit(r#"{"flag":true}"#),
1519                ),
1520            ],
1521        };
1522
1523        let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1524        assert!(annotations.has_turbopack_loader());
1525
1526        let loader = annotations.turbopack_loader().unwrap();
1527        assert_eq!(loader.loader.as_str(), "my-loader");
1528        assert_eq!(loader.options["flag"], serde_json::Value::Bool(true));
1529    }
1530
1531    #[test]
1532    fn test_parse_without_turbopack_loader() {
1533        // Simulate: with { type: "json" }
1534        let with = ObjectLit {
1535            span: DUMMY_SP,
1536            props: vec![kv_prop(ident_key("type"), str_lit("json"))],
1537        };
1538
1539        let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1540        assert!(!annotations.has_turbopack_loader());
1541        assert!(annotations.module_type().is_some());
1542    }
1543
1544    #[test]
1545    fn test_parse_empty_with() {
1546        let annotations = ImportAnnotations::parse(None);
1547        assert!(annotations.is_none());
1548    }
1549}