Skip to main content

turbopack_ecmascript/analyzer/
imports.rs

1use std::{borrow::Cow, collections::BTreeMap, fmt::Display, sync::Arc};
2
3use once_cell::sync::Lazy;
4use rustc_hash::{FxHashMap, FxHashSet};
5use smallvec::SmallVec;
6use swc_core::{
7    atoms::Wtf8Atom,
8    common::{BytePos, Span, Spanned, SyntaxContext, comments::Comments, source_map::SmallPos},
9    ecma::{
10        ast::*,
11        atoms::{Atom, atom},
12        utils::{IsDirective, find_pat_ids},
13        visit::{Visit, VisitWith},
14    },
15};
16use turbo_rcstr::{RcStr, rcstr};
17use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc};
18use turbopack_core::{issue::IssueSource, loader::WebpackLoaderItem, source::Source};
19
20use super::{JsValue, ModuleValue, top_level_await::has_top_level_await};
21use crate::{
22    SpecifiedModuleType,
23    analyzer::{ConstantValue, ObjectPart},
24    magic_identifier,
25    references::util::{SpecifiedChunkingType, parse_chunking_type_annotation},
26    tree_shake::{PartId, find_turbopack_part_id_in_asserts},
27};
28
29#[turbo_tasks::value]
30#[derive(Default, Debug, Clone, Hash)]
31pub struct ImportAnnotations {
32    // TODO store this in more structured way
33    #[turbo_tasks(trace_ignore)]
34    #[bincode(with_serde)]
35    map: BTreeMap<Wtf8Atom, Wtf8Atom>,
36    /// Parsed turbopack loader configuration from import attributes.
37    /// e.g. `import "file" with { turbopackLoader: "raw-loader" }`
38    #[turbo_tasks(trace_ignore)]
39    #[bincode(with_serde)]
40    turbopack_loader: Option<WebpackLoaderItem>,
41    turbopack_rename_as: Option<RcStr>,
42    turbopack_module_type: Option<RcStr>,
43    chunking_type: Option<SpecifiedChunkingType>,
44}
45
46/// Enables a specified transition for the annotated import
47static ANNOTATION_TRANSITION: Lazy<Wtf8Atom> =
48    Lazy::new(|| crate::annotations::ANNOTATION_TRANSITION.into());
49
50/// Changes the type of the resolved module (only "json" is supported currently)
51static ATTRIBUTE_MODULE_TYPE: Lazy<Wtf8Atom> = Lazy::new(|| atom!("type").into());
52
53impl ImportAnnotations {
54    pub fn parse(with: Option<&ObjectLit>) -> Option<ImportAnnotations> {
55        let with = with?;
56
57        let mut map = BTreeMap::new();
58        let mut turbopack_loader_name: Option<RcStr> = None;
59        let mut turbopack_loader_options: serde_json::Map<String, serde_json::Value> =
60            serde_json::Map::new();
61        let mut turbopack_rename_as: Option<RcStr> = None;
62        let mut turbopack_module_type: Option<RcStr> = None;
63        let mut chunking_type: Option<SpecifiedChunkingType> = None;
64
65        for prop in &with.props {
66            let Some(kv) = prop.as_prop().and_then(|p| p.as_key_value()) else {
67                continue;
68            };
69
70            let key_str = match &kv.key {
71                PropName::Ident(ident) => Cow::Borrowed(ident.sym.as_str()),
72                PropName::Str(str) => str.value.to_string_lossy(),
73                _ => continue,
74            };
75
76            // All turbopack* keys are extracted as string values (per TC39 import attributes spec)
77            match &*key_str {
78                "turbopackLoader" => {
79                    if let Some(Lit::Str(s)) = kv.value.as_lit() {
80                        turbopack_loader_name =
81                            Some(RcStr::from(s.value.to_string_lossy().into_owned()));
82                    }
83                }
84                "turbopackLoaderOptions" => {
85                    if let Some(Lit::Str(s)) = kv.value.as_lit() {
86                        let json_str = s.value.to_string_lossy();
87                        if let Ok(serde_json::Value::Object(map)) = serde_json::from_str(&json_str)
88                        {
89                            turbopack_loader_options = map;
90                        }
91                    }
92                }
93                "turbopackAs" => {
94                    if let Some(Lit::Str(s)) = kv.value.as_lit() {
95                        turbopack_rename_as =
96                            Some(RcStr::from(s.value.to_string_lossy().into_owned()));
97                    }
98                }
99                "turbopackModuleType" => {
100                    if let Some(Lit::Str(s)) = kv.value.as_lit() {
101                        turbopack_module_type =
102                            Some(RcStr::from(s.value.to_string_lossy().into_owned()));
103                    }
104                }
105                "turbopack-chunking-type" => {
106                    if let Some(Lit::Str(s)) = kv.value.as_lit() {
107                        chunking_type = parse_chunking_type_annotation(
108                            kv.value.span(),
109                            &s.value.to_string_lossy(),
110                        );
111                    }
112                }
113                _ => {
114                    // For all other keys, only accept string values (per spec)
115                    if let Some(Lit::Str(str)) = kv.value.as_lit() {
116                        let key: Wtf8Atom = match &kv.key {
117                            PropName::Ident(ident) => ident.sym.clone().into(),
118                            PropName::Str(s) => s.value.clone(),
119                            _ => continue,
120                        };
121                        map.insert(key, str.value.clone());
122                    }
123                }
124            }
125        }
126
127        let turbopack_loader = turbopack_loader_name.map(|name| WebpackLoaderItem {
128            loader: name,
129            options: turbopack_loader_options,
130        });
131
132        if !map.is_empty()
133            || turbopack_loader.is_some()
134            || turbopack_rename_as.is_some()
135            || turbopack_module_type.is_some()
136            || chunking_type.is_some()
137        {
138            Some(ImportAnnotations {
139                map,
140                turbopack_loader,
141                turbopack_rename_as,
142                turbopack_module_type,
143                chunking_type,
144            })
145        } else {
146            None
147        }
148    }
149
150    pub fn parse_dynamic(with: &JsValue) -> Option<ImportAnnotations> {
151        let mut map = BTreeMap::new();
152
153        let JsValue::Object { parts, .. } = with else {
154            return None;
155        };
156
157        for part in parts.iter() {
158            let ObjectPart::KeyValue(key, value) = part else {
159                continue;
160            };
161            let (
162                JsValue::Constant(ConstantValue::Str(key)),
163                JsValue::Constant(ConstantValue::Str(value)),
164            ) = (key, value)
165            else {
166                continue;
167            };
168
169            map.insert(
170                key.as_atom().into_owned().into(),
171                value.as_atom().into_owned().into(),
172            );
173        }
174
175        if !map.is_empty() {
176            Some(ImportAnnotations {
177                map,
178                turbopack_loader: None,
179                turbopack_rename_as: None,
180                turbopack_module_type: None,
181                chunking_type: None,
182            })
183        } else {
184            None
185        }
186    }
187
188    /// Returns the content on the transition annotation
189    pub fn transition(&self) -> Option<Cow<'_, str>> {
190        self.get(&ANNOTATION_TRANSITION)
191            .map(|v| v.to_string_lossy())
192    }
193
194    /// Returns the content on the chunking-type annotation
195    pub fn chunking_type(&self) -> Option<SpecifiedChunkingType> {
196        self.chunking_type
197    }
198
199    /// Returns the content on the type attribute
200    pub fn module_type(&self) -> Option<&Wtf8Atom> {
201        self.get(&ATTRIBUTE_MODULE_TYPE)
202    }
203
204    /// Returns the turbopackLoader item, if present
205    pub fn turbopack_loader(&self) -> Option<&WebpackLoaderItem> {
206        self.turbopack_loader.as_ref()
207    }
208
209    /// Returns the turbopackAs rename configuration, if present
210    pub fn turbopack_rename_as(&self) -> Option<&RcStr> {
211        self.turbopack_rename_as.as_ref()
212    }
213
214    /// Returns the turbopackModuleType override, if present
215    pub fn turbopack_module_type(&self) -> Option<&RcStr> {
216        self.turbopack_module_type.as_ref()
217    }
218
219    /// Returns true if a turbopack loader is configured
220    pub fn has_turbopack_loader(&self) -> bool {
221        self.turbopack_loader.is_some()
222    }
223
224    pub fn get(&self, key: &Wtf8Atom) -> Option<&Wtf8Atom> {
225        self.map.get(key)
226    }
227}
228
229impl Display for ImportAnnotations {
230    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231        let mut it = self.map.iter();
232        if let Some((k, v)) = it.next() {
233            write!(f, "{{ {}: {}", k.to_string_lossy(), v.to_string_lossy())?
234        } else {
235            return f.write_str("{}");
236        };
237        for (k, v) in it {
238            write!(f, ", {}: {}", k.to_string_lossy(), v.to_string_lossy())?
239        }
240        f.write_str(" }")
241    }
242}
243
244#[derive(Debug)]
245pub(crate) enum Reexport {
246    Star,
247    Namespace { exported: Atom },
248    Named { imported: Atom, exported: Atom },
249}
250
251/// The storage for all kinds of imports.
252///
253/// Note that when it's initialized by calling `analyze`, it only contains ESM
254/// import/exports.
255#[derive(Default, Debug)]
256pub(crate) struct ImportMap {
257    /// Map from identifier to (index in references, exported symbol)
258    imports: FxIndexMap<Id, (usize, Atom)>,
259
260    /// Map from identifier to index in references
261    namespace_imports: FxIndexMap<Id, usize>,
262
263    /// List of (index in references, imported symbol, exported symbol)
264    reexports: Vec<(usize, Reexport)>,
265
266    /// Ordered list of imported symbols
267    references: FxIndexSet<ImportMapReference>,
268
269    /// True, when the module has imports
270    has_imports: bool,
271
272    /// True, when the module has exports
273    has_exports: bool,
274
275    /// True if the module is an ESM module due to top-level await.
276    has_top_level_await: bool,
277
278    /// True if the module has "use strict"
279    pub(crate) strict: bool,
280
281    /// Locations of [webpack-style "magic comments"][magic] that override import behaviors.
282    ///
283    /// Most commonly, these are `/* webpackIgnore: true */` comments. See [ImportAttributes] for
284    /// full details.
285    ///
286    /// [magic]: https://webpack.js.org/api/module-methods/#magic-comments
287    attributes: FxHashMap<BytePos, ImportAttributes>,
288
289    /// The module specifiers of star imports that are accessed dynamically and should be imported
290    /// as a whole.
291    full_star_imports: FxHashSet<Wtf8Atom>,
292
293    pub(crate) exports: FxHashMap<RcStr, Id>,
294}
295
296/// Represents a collection of [webpack-style "magic comments"][magic] that override import
297/// behaviors.
298///
299/// [magic]: https://webpack.js.org/api/module-methods/#magic-comments
300#[derive(Debug)]
301pub struct ImportAttributes {
302    /// Should we ignore this import expression when bundling? If so, the import expression will be
303    /// left as-is in Turbopack's output.
304    ///
305    /// This is set by using either a `webpackIgnore` or `turbopackIgnore` comment.
306    ///
307    /// Example:
308    /// ```js
309    /// const a = import(/* webpackIgnore: true */ "a");
310    /// const b = import(/* turbopackIgnore: true */ "b");
311    /// ```
312    pub ignore: bool,
313    /// Should resolution errors be suppressed? If so, resolution errors will be completely
314    /// ignored (no error or warning emitted at build time).
315    ///
316    /// This is set by using a `turbopackOptional` comment.
317    ///
318    /// Example:
319    /// ```js
320    /// const a = import(/* turbopackOptional: true */ "a");
321    /// ```
322    pub optional: bool,
323    /// Which exports are used from a dynamic import. When set, enables tree-shaking for the
324    /// dynamically imported module by only including the specified exports.
325    ///
326    /// This is set by using either a `webpackExports` or `turbopackExports` comment.
327    /// `None` means no directive was found (all exports assumed used).
328    /// `Some([])` means empty list (only side effects).
329    /// `Some([name, ...])` means specific named exports are used.
330    ///
331    /// Example:
332    /// ```js
333    /// const { a } = await import(/* webpackExports: ["a"] */ "module");
334    /// const { b } = await import(/* turbopackExports: "b" */ "module");
335    /// ```
336    pub export_names: Option<SmallVec<[RcStr; 1]>>,
337    /// Whether to use a specific chunking type for this import.
338    //
339    /// This is set by using a or `turbopackChunkingType` comment.
340    ///
341    /// Example:
342    /// ```js
343    /// const a = require(/* turbopackChunkingType: parallel */ "a");
344    /// ```
345    pub chunking_type: Option<SpecifiedChunkingType>,
346}
347
348impl ImportAttributes {
349    pub const fn empty() -> Self {
350        ImportAttributes {
351            ignore: false,
352            optional: false,
353            export_names: None,
354            chunking_type: None,
355        }
356    }
357
358    pub fn empty_ref() -> &'static Self {
359        // use `Self::empty` here as `Default::default` isn't const
360        static DEFAULT_VALUE: ImportAttributes = ImportAttributes::empty();
361        &DEFAULT_VALUE
362    }
363}
364
365impl Default for ImportAttributes {
366    fn default() -> Self {
367        ImportAttributes::empty()
368    }
369}
370
371impl Default for &ImportAttributes {
372    fn default() -> Self {
373        ImportAttributes::empty_ref()
374    }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq, Hash)]
378pub(crate) enum ImportedSymbol {
379    ModuleEvaluation,
380    Symbol(Atom),
381    Exports,
382    Part(u32),
383    PartEvaluation(u32),
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Hash)]
387pub(crate) struct ImportMapReference {
388    pub module_path: Wtf8Atom,
389    pub imported_symbol: ImportedSymbol,
390    pub annotations: Option<Arc<ImportAnnotations>>,
391    pub issue_source: Option<IssueSource>,
392}
393
394impl ImportMap {
395    pub fn is_esm(&self, specified_type: SpecifiedModuleType) -> bool {
396        if self.has_exports {
397            return true;
398        }
399
400        match specified_type {
401            SpecifiedModuleType::Automatic => {
402                self.has_exports || self.has_imports || self.has_top_level_await
403            }
404            SpecifiedModuleType::CommonJs => false,
405            SpecifiedModuleType::EcmaScript => true,
406        }
407    }
408
409    pub fn get_import(&self, id: &Id) -> Option<JsValue> {
410        if let Some((i, i_sym)) = self.imports.get(id) {
411            let r = &self.references[*i];
412            return Some(JsValue::member(
413                Box::new(JsValue::Module(ModuleValue {
414                    module: r.module_path.clone(),
415                    annotations: r.annotations.clone(),
416                })),
417                Box::new(i_sym.clone().into()),
418            ));
419        }
420        if let Some(i) = self.namespace_imports.get(id) {
421            let r = &self.references[*i];
422            return Some(JsValue::Module(ModuleValue {
423                module: r.module_path.clone(),
424                annotations: r.annotations.clone(),
425            }));
426        }
427        None
428    }
429
430    pub fn get_attributes(&self, span: Span) -> &ImportAttributes {
431        self.attributes.get(&span.lo).unwrap_or_default()
432    }
433
434    // TODO this could return &str instead of String to avoid cloning
435    pub fn get_binding(&self, id: &Id) -> Option<(usize, Option<RcStr>)> {
436        if let Some((i, i_sym)) = self.imports.get(id) {
437            return Some((*i, Some(i_sym.as_str().into())));
438        }
439        if let Some(i) = self.namespace_imports.get(id) {
440            return Some((*i, None));
441        }
442        None
443    }
444
445    pub fn references(&self) -> impl ExactSizeIterator<Item = &ImportMapReference> {
446        self.references.iter()
447    }
448
449    pub fn reexports(&self) -> impl ExactSizeIterator<Item = (usize, &Reexport)> {
450        self.reexports.iter().map(|(i, r)| (*i, r))
451    }
452
453    /// Analyze ES import
454    pub(super) fn analyze(
455        m: &Program,
456        source: Option<ResolvedVc<Box<dyn Source>>>,
457        comments: Option<&dyn Comments>,
458    ) -> Self {
459        let mut data = ImportMap::default();
460
461        // We have to analyze imports first to determine if a star import is dynamic.
462        // We can't do this in the visitor because import may (and likely) comes before usages, and
463        // a method invoked after visitor will not work because we need to preserve the import
464        // order.
465
466        if let Program::Module(m) = m {
467            let mut candidates = FxIndexMap::default();
468
469            // Imports are hoisted to the top of the module.
470            // So we have to collect all imports first.
471            m.body.iter().for_each(|stmt| {
472                if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = stmt {
473                    for s in &import.specifiers {
474                        if let ImportSpecifier::Namespace(s) = s {
475                            candidates.insert(s.local.to_id(), import.src.value.clone());
476                        }
477                    }
478                }
479            });
480
481            let mut analyzer = StarImportAnalyzer {
482                candidates,
483                full_star_imports: &mut data.full_star_imports,
484            };
485            m.visit_with(&mut analyzer);
486        }
487
488        let mut analyzer = Analyzer {
489            data: &mut data,
490            source,
491            comments,
492        };
493        m.visit_with(&mut analyzer);
494
495        data
496    }
497
498    pub(crate) fn should_import_all(&self, esm_reference_index: usize) -> bool {
499        let r = &self.references[esm_reference_index];
500
501        self.full_star_imports.contains(&r.module_path)
502    }
503}
504
505struct StarImportAnalyzer<'a> {
506    /// The local identifiers of the star imports
507    candidates: FxIndexMap<Id, Wtf8Atom>,
508    full_star_imports: &'a mut FxHashSet<Wtf8Atom>,
509}
510
511impl Visit for StarImportAnalyzer<'_> {
512    fn visit_expr(&mut self, node: &Expr) {
513        if let Expr::Ident(i) = node
514            && let Some(module_path) = self.candidates.get(&i.to_id())
515        {
516            self.full_star_imports.insert(module_path.clone());
517            return;
518        }
519
520        node.visit_children_with(self);
521    }
522
523    fn visit_import_decl(&mut self, _: &ImportDecl) {}
524
525    fn visit_member_expr(&mut self, node: &MemberExpr) {
526        match &node.prop {
527            MemberProp::Ident(..) | MemberProp::PrivateName(..) => {
528                if node.obj.is_ident() {
529                    return;
530                }
531                // We can skip `visit_expr(obj)` because it's not a dynamic access
532                node.obj.visit_children_with(self);
533            }
534            MemberProp::Computed(..) => {
535                node.obj.visit_with(self);
536                node.prop.visit_with(self);
537            }
538        }
539    }
540
541    fn visit_pat(&mut self, pat: &Pat) {
542        if let Pat::Ident(i) = pat
543            && let Some(module_path) = self.candidates.get(&i.to_id())
544        {
545            self.full_star_imports.insert(module_path.clone());
546            return;
547        }
548
549        pat.visit_children_with(self);
550    }
551
552    fn visit_simple_assign_target(&mut self, node: &SimpleAssignTarget) {
553        if let SimpleAssignTarget::Ident(i) = node
554            && let Some(module_path) = self.candidates.get(&i.to_id())
555        {
556            self.full_star_imports.insert(module_path.clone());
557            return;
558        }
559
560        node.visit_children_with(self);
561    }
562}
563
564struct Analyzer<'a> {
565    data: &'a mut ImportMap,
566    source: Option<ResolvedVc<Box<dyn Source>>>,
567    comments: Option<&'a dyn Comments>,
568}
569
570impl Analyzer<'_> {
571    fn ensure_reference(
572        &mut self,
573        span: Span,
574        module_path: Wtf8Atom,
575        imported_symbol: ImportedSymbol,
576        annotations: Option<ImportAnnotations>,
577    ) -> Option<usize> {
578        let issue_source = self
579            .source
580            .map(|s| IssueSource::from_swc_offsets(s, span.lo.to_u32(), span.hi.to_u32()));
581
582        let r = ImportMapReference {
583            module_path,
584            imported_symbol,
585            issue_source,
586            annotations: annotations.map(Arc::new),
587        };
588        if let Some(i) = self.data.references.get_index_of(&r) {
589            Some(i)
590        } else {
591            let i = self.data.references.len();
592            self.data.references.insert(r);
593            Some(i)
594        }
595    }
596}
597
598impl Visit for Analyzer<'_> {
599    fn visit_import_decl(&mut self, import: &ImportDecl) {
600        self.data.has_imports = true;
601
602        let annotations = ImportAnnotations::parse(import.with.as_deref());
603
604        let internal_symbol = parse_with(import.with.as_deref());
605
606        if internal_symbol.is_none() {
607            self.ensure_reference(
608                import.span,
609                import.src.value.clone(),
610                ImportedSymbol::ModuleEvaluation,
611                annotations.clone(),
612            );
613        }
614
615        for s in &import.specifiers {
616            let symbol = internal_symbol
617                .clone()
618                .unwrap_or_else(|| get_import_symbol_from_import(s));
619            let i = self.ensure_reference(
620                import.span,
621                import.src.value.clone(),
622                symbol,
623                annotations.clone(),
624            );
625            let i = match i {
626                Some(v) => v,
627                None => continue,
628            };
629
630            let (local, orig_sym) = match s {
631                ImportSpecifier::Named(ImportNamedSpecifier {
632                    local, imported, ..
633                }) => match imported {
634                    Some(imported) => (local.to_id(), imported.atom().into_owned()),
635                    _ => (local.to_id(), local.sym.clone()),
636                },
637                ImportSpecifier::Default(s) => (s.local.to_id(), atom!("default")),
638                ImportSpecifier::Namespace(s) => {
639                    self.data.namespace_imports.insert(s.local.to_id(), i);
640                    continue;
641                }
642            };
643
644            self.data.imports.insert(local, (i, orig_sym));
645        }
646        if import.specifiers.is_empty()
647            && let Some(internal_symbol) = internal_symbol
648        {
649            self.ensure_reference(
650                import.span,
651                import.src.value.clone(),
652                internal_symbol,
653                annotations,
654            );
655        }
656    }
657
658    fn visit_export_all(&mut self, export: &ExportAll) {
659        self.data.has_exports = true;
660
661        let annotations = ImportAnnotations::parse(export.with.as_deref());
662
663        self.ensure_reference(
664            export.span,
665            export.src.value.clone(),
666            ImportedSymbol::ModuleEvaluation,
667            annotations.clone(),
668        );
669        let symbol = parse_with(export.with.as_deref());
670
671        let i = self.ensure_reference(
672            export.span,
673            export.src.value.clone(),
674            symbol.unwrap_or(ImportedSymbol::Exports),
675            annotations,
676        );
677        if let Some(i) = i {
678            self.data.reexports.push((i, Reexport::Star));
679        }
680    }
681
682    fn visit_named_export(&mut self, export: &NamedExport) {
683        self.data.has_exports = true;
684
685        let Some(ref src) = export.src else {
686            export.visit_children_with(self);
687            return;
688        };
689
690        let annotations = ImportAnnotations::parse(export.with.as_deref());
691
692        let internal_symbol = parse_with(export.with.as_deref());
693
694        if internal_symbol.is_none() || export.specifiers.is_empty() {
695            self.ensure_reference(
696                export.span,
697                src.value.clone(),
698                ImportedSymbol::ModuleEvaluation,
699                annotations.clone(),
700            );
701        }
702
703        for spec in export.specifiers.iter() {
704            let symbol = internal_symbol
705                .clone()
706                .unwrap_or_else(|| get_import_symbol_from_export(spec));
707
708            let i =
709                self.ensure_reference(export.span, src.value.clone(), symbol, annotations.clone());
710            let i = match i {
711                Some(v) => v,
712                None => continue,
713            };
714
715            match spec {
716                ExportSpecifier::Namespace(n) => {
717                    self.data.reexports.push((
718                        i,
719                        Reexport::Namespace {
720                            exported: n.name.atom().into_owned(),
721                        },
722                    ));
723                }
724                ExportSpecifier::Default(d) => {
725                    self.data.reexports.push((
726                        i,
727                        Reexport::Named {
728                            imported: atom!("default"),
729                            exported: d.exported.sym.clone(),
730                        },
731                    ));
732                }
733                ExportSpecifier::Named(n) => {
734                    self.data.reexports.push((
735                        i,
736                        Reexport::Named {
737                            imported: n.orig.atom().into_owned(),
738                            exported: n.exported.as_ref().unwrap_or(&n.orig).atom().into_owned(),
739                        },
740                    ));
741                }
742            }
743        }
744    }
745
746    fn visit_export_decl(&mut self, n: &ExportDecl) {
747        self.data.has_exports = true;
748
749        if self.comments.is_some() {
750            // only visit children if we potentially need to mark import / requires
751            n.visit_children_with(self);
752        }
753
754        match &n.decl {
755            Decl::Class(n) => {
756                self.data
757                    .exports
758                    .insert(n.ident.sym.as_str().into(), n.ident.to_id());
759            }
760            Decl::Fn(n) => {
761                self.data
762                    .exports
763                    .insert(n.ident.sym.as_str().into(), n.ident.to_id());
764            }
765            Decl::Var(..) | Decl::Using(..) => {
766                let ids: Vec<Id> = find_pat_ids(&n.decl);
767                for id in ids {
768                    self.data.exports.insert(id.0.as_str().into(), id);
769                }
770            }
771            _ => {}
772        }
773    }
774
775    fn visit_export_default_decl(&mut self, n: &ExportDefaultDecl) {
776        self.data.has_exports = true;
777
778        if self.comments.is_some() {
779            // only visit children if we potentially need to mark import / requires
780            n.visit_children_with(self);
781        }
782
783        self.data.exports.insert(
784            rcstr!("default"),
785            // Mirror what `EsmModuleItem::code_generation` does, these are live bindings if the
786            // class/function has an identifier.
787            match &n.decl {
788                DefaultDecl::Class(ClassExpr { ident, .. })
789                | DefaultDecl::Fn(FnExpr { ident, .. }) => ident.as_ref().map_or_else(
790                    || {
791                        (
792                            magic_identifier::mangle("default export").into(),
793                            SyntaxContext::empty(),
794                        )
795                    },
796                    |ident| ident.to_id(),
797                ),
798                DefaultDecl::TsInterfaceDecl(_) => {
799                    // not matching, might happen due to eventual consistency
800                    (
801                        magic_identifier::mangle("default export").into(),
802                        SyntaxContext::empty(),
803                    )
804                }
805            },
806        );
807    }
808
809    fn visit_export_default_expr(&mut self, n: &ExportDefaultExpr) {
810        self.data.has_exports = true;
811
812        if self.comments.is_some() {
813            // only visit children if we potentially need to mark import / requires
814            n.visit_children_with(self);
815        }
816
817        self.data.exports.insert(
818            rcstr!("default"),
819            (
820                // `EsmModuleItem::code_generation` inserts this variable.
821                magic_identifier::mangle("default export").into(),
822                SyntaxContext::empty(),
823            ),
824        );
825    }
826
827    fn visit_export_named_specifier(&mut self, n: &ExportNamedSpecifier) {
828        let ModuleExportName::Ident(local) = &n.orig else {
829            // This is only possible for re-exports, but they are already handled earlier in
830            // visit_named_export.
831            unreachable!("string reexports should have been already handled in visit_named_export");
832        };
833        let exported = n.exported.as_ref().unwrap_or(&n.orig);
834        self.data
835            .exports
836            .insert(exported.atom().as_str().into(), local.to_id());
837    }
838
839    fn visit_export_default_specifier(&mut self, n: &ExportDefaultSpecifier) {
840        self.data
841            .exports
842            .insert(rcstr!("default"), n.exported.to_id());
843    }
844
845    fn visit_program(&mut self, m: &Program) {
846        self.data.has_top_level_await = has_top_level_await(m).is_some();
847        self.data.strict = match m {
848            Program::Module(module) => module
849                .body
850                .iter()
851                .take_while(|s| s.directive_continue())
852                .any(IsDirective::is_use_strict),
853            Program::Script(script) => script
854                .body
855                .iter()
856                .take_while(|s| s.directive_continue())
857                .any(IsDirective::is_use_strict),
858        };
859
860        m.visit_children_with(self);
861    }
862
863    fn visit_stmt(&mut self, n: &Stmt) {
864        if self.comments.is_some() {
865            // only visit children if we potentially need to mark import / requires
866            n.visit_children_with(self);
867        }
868    }
869
870    /// check if import or require contains magic comments
871    ///
872    /// We are checking for the following cases:
873    /// - import(/* webpackIgnore: true */ "a")
874    /// - require(/* webpackIgnore: true */ "a")
875    /// - import(/* turbopackOptional: true */ "a")
876    /// - require(/* turbopackOptional: true */ "a")
877    ///
878    /// We can do this by checking if any of the comment spans are between the
879    /// callee and the first argument.
880    //
881    // potentially support more webpack magic comments in the future:
882    // https://webpack.js.org/api/module-methods/#magic-comments
883    fn visit_call_expr(&mut self, n: &CallExpr) {
884        // we could actually unwrap thanks to the optimisation above but it can't hurt to be safe...
885        if let Some(comments) = self.comments {
886            let callee_span = match &n.callee {
887                Callee::Import(Import { span, .. }) => Some(*span),
888                Callee::Expr(e) => Some(e.span()),
889                _ => None,
890            };
891
892            if let Some(callee_span) = callee_span
893                && let Some(attributes) = parse_directives(comments, n.args.first())
894            {
895                self.data.attributes.insert(callee_span.lo, attributes);
896            }
897        }
898
899        n.visit_children_with(self);
900    }
901
902    fn visit_new_expr(&mut self, n: &NewExpr) {
903        // we could actually unwrap thanks to the optimisation above but it can't hurt to be safe...
904        if let Some(comments) = self.comments {
905            let callee_span = match &n.callee {
906                box Expr::Ident(Ident { sym, .. }) if sym == "Worker" => Some(n.span),
907                _ => None,
908            };
909
910            if let Some(callee_span) = callee_span
911                && let Some(attributes) = parse_directives(comments, n.args.iter().flatten().next())
912            {
913                self.data.attributes.insert(callee_span.lo, attributes);
914            }
915        }
916
917        n.visit_children_with(self);
918    }
919}
920
921/// Parse magic comment directives from the leading comments of a call argument.
922/// Returns (ignore, optional) directives if any are found.
923fn parse_directives(
924    comments: &dyn Comments,
925    value: Option<&ExprOrSpread>,
926) -> Option<ImportAttributes> {
927    let value = value?;
928    let leading_comments = comments.get_leading(value.span_lo())?;
929
930    let mut ignore = None;
931    let mut optional = None;
932    let mut export_names = None;
933    let mut chunking_type = None;
934
935    // Process all comments, last one wins for each directive type
936    for comment in leading_comments.iter() {
937        if let Some((directive, val)) = comment.text.trim().split_once(':') {
938            let val = val.trim();
939            match directive.trim() {
940                "webpackIgnore" | "turbopackIgnore" => match val {
941                    "true" => ignore = Some(true),
942                    "false" => ignore = Some(false),
943                    _ => {}
944                },
945                "turbopackOptional" => match val {
946                    "true" => optional = Some(true),
947                    "false" => optional = Some(false),
948                    _ => {}
949                },
950                "webpackExports" | "turbopackExports" => {
951                    export_names = Some(parse_export_names(val));
952                }
953                "turbopackChunkingType" => {
954                    chunking_type = parse_chunking_type_annotation(value.span(), val);
955                }
956                _ => {} // ignore anything else
957            }
958        }
959    }
960
961    // Return Some only if at least one directive was found
962    if ignore.is_some() || optional.is_some() || export_names.is_some() || chunking_type.is_some() {
963        Some(ImportAttributes {
964            ignore: ignore.unwrap_or(false),
965            optional: optional.unwrap_or(false),
966            export_names,
967            chunking_type,
968        })
969    } else {
970        None
971    }
972}
973
974/// Parse export names from a `webpackExports` or `turbopackExports` comment value.
975///
976/// Supports two formats:
977/// - Single string: `"name"` → `["name"]`
978/// - JSON array: `["name1", "name2"]` → `["name1", "name2"]`
979fn parse_export_names(val: &str) -> SmallVec<[RcStr; 1]> {
980    let val = val.trim();
981
982    // Try parsing as JSON array of strings
983    if let Ok(names) = serde_json::from_str::<Vec<String>>(val) {
984        return names.into_iter().map(|s| s.into()).collect();
985    }
986
987    // Try parsing as a single JSON string
988    if let Ok(name) = serde_json::from_str::<String>(val) {
989        return SmallVec::from_buf([name.into()]);
990    }
991
992    // Bare identifier (no quotes)
993    if !val.is_empty() {
994        return SmallVec::from_buf([val.into()]);
995    }
996
997    SmallVec::new()
998}
999
1000fn parse_with(with: Option<&ObjectLit>) -> Option<ImportedSymbol> {
1001    find_turbopack_part_id_in_asserts(with?).map(|v| match v {
1002        PartId::Internal(index, true) => ImportedSymbol::PartEvaluation(index),
1003        PartId::Internal(index, false) => ImportedSymbol::Part(index),
1004        PartId::ModuleEvaluation => ImportedSymbol::ModuleEvaluation,
1005        PartId::Export(e) => ImportedSymbol::Symbol(e.as_str().into()),
1006        PartId::Exports => ImportedSymbol::Exports,
1007    })
1008}
1009
1010fn get_import_symbol_from_import(specifier: &ImportSpecifier) -> ImportedSymbol {
1011    match specifier {
1012        ImportSpecifier::Named(ImportNamedSpecifier {
1013            local, imported, ..
1014        }) => ImportedSymbol::Symbol(match imported {
1015            Some(imported) => imported.atom().into_owned(),
1016            _ => local.sym.clone(),
1017        }),
1018        ImportSpecifier::Default(..) => ImportedSymbol::Symbol(atom!("default")),
1019        ImportSpecifier::Namespace(..) => ImportedSymbol::Exports,
1020    }
1021}
1022
1023fn get_import_symbol_from_export(specifier: &ExportSpecifier) -> ImportedSymbol {
1024    match specifier {
1025        ExportSpecifier::Named(ExportNamedSpecifier { orig, .. }) => {
1026            ImportedSymbol::Symbol(orig.atom().into_owned())
1027        }
1028        ExportSpecifier::Default(..) => ImportedSymbol::Symbol(atom!("default")),
1029        ExportSpecifier::Namespace(..) => ImportedSymbol::Exports,
1030    }
1031}
1032
1033#[cfg(test)]
1034mod tests {
1035    use swc_core::{atoms::Atom, common::DUMMY_SP, ecma::ast::*};
1036
1037    use super::*;
1038
1039    /// Helper to create a string literal expression
1040    fn str_lit(s: &str) -> Box<Expr> {
1041        Box::new(Expr::Lit(Lit::Str(Str {
1042            span: DUMMY_SP,
1043            value: Atom::from(s).into(),
1044            raw: None,
1045        })))
1046    }
1047
1048    /// Helper to create an ident property name
1049    fn ident_key(s: &str) -> PropName {
1050        PropName::Ident(IdentName {
1051            span: DUMMY_SP,
1052            sym: Atom::from(s),
1053        })
1054    }
1055
1056    /// Helper to create a key-value property
1057    fn kv_prop(key: PropName, value: Box<Expr>) -> PropOrSpread {
1058        PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { key, value })))
1059    }
1060
1061    #[test]
1062    fn test_parse_turbopack_loader_annotation() {
1063        // Simulate: with { turbopackLoader: "raw-loader" }
1064        let with = ObjectLit {
1065            span: DUMMY_SP,
1066            props: vec![kv_prop(ident_key("turbopackLoader"), str_lit("raw-loader"))],
1067        };
1068
1069        let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1070        assert!(annotations.has_turbopack_loader());
1071
1072        let loader = annotations.turbopack_loader().unwrap();
1073        assert_eq!(loader.loader.as_str(), "raw-loader");
1074        assert!(loader.options.is_empty());
1075    }
1076
1077    #[test]
1078    fn test_parse_turbopack_loader_with_options() {
1079        // Simulate: with { turbopackLoader: "my-loader", turbopackLoaderOptions: '{"flag":true}' }
1080        let with = ObjectLit {
1081            span: DUMMY_SP,
1082            props: vec![
1083                kv_prop(ident_key("turbopackLoader"), str_lit("my-loader")),
1084                kv_prop(
1085                    ident_key("turbopackLoaderOptions"),
1086                    str_lit(r#"{"flag":true}"#),
1087                ),
1088            ],
1089        };
1090
1091        let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1092        assert!(annotations.has_turbopack_loader());
1093
1094        let loader = annotations.turbopack_loader().unwrap();
1095        assert_eq!(loader.loader.as_str(), "my-loader");
1096        assert_eq!(loader.options["flag"], serde_json::Value::Bool(true));
1097    }
1098
1099    #[test]
1100    fn test_parse_without_turbopack_loader() {
1101        // Simulate: with { type: "json" }
1102        let with = ObjectLit {
1103            span: DUMMY_SP,
1104            props: vec![kv_prop(ident_key("type"), str_lit("json"))],
1105        };
1106
1107        let annotations = ImportAnnotations::parse(Some(&with)).unwrap();
1108        assert!(!annotations.has_turbopack_loader());
1109        assert!(annotations.module_type().is_some());
1110    }
1111
1112    #[test]
1113    fn test_parse_empty_with() {
1114        let annotations = ImportAnnotations::parse(None);
1115        assert!(annotations.is_none());
1116    }
1117}