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