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