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