turbopack_ecmascript/analyzer/
imports.rs

1use std::{borrow::Cow, collections::BTreeMap, fmt::Display};
2
3use once_cell::sync::Lazy;
4use rustc_hash::{FxHashMap, FxHashSet};
5use swc_core::{
6    atoms::Wtf8Atom,
7    common::{BytePos, Span, Spanned, SyntaxContext, comments::Comments, source_map::SmallPos},
8    ecma::{
9        ast::*,
10        atoms::{Atom, atom},
11        utils::{IsDirective, find_pat_ids},
12        visit::{Visit, VisitWith},
13    },
14};
15use turbo_rcstr::{RcStr, rcstr};
16use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc};
17use turbopack_core::{issue::IssueSource, source::Source};
18
19use super::{JsValue, ModuleValue, top_level_await::has_top_level_await};
20use crate::{
21    SpecifiedModuleType,
22    analyzer::{ConstantValue, ObjectPart},
23    magic_identifier,
24    tree_shake::{PartId, find_turbopack_part_id_in_asserts},
25};
26
27#[turbo_tasks::value]
28#[derive(Default, Debug, Clone, Hash)]
29pub struct ImportAnnotations {
30    // TODO store this in more structured way
31    #[turbo_tasks(trace_ignore)]
32    #[bincode(with_serde)]
33    map: BTreeMap<Wtf8Atom, Wtf8Atom>,
34}
35
36/// Enables a specified transition for the annotated import
37static ANNOTATION_TRANSITION: Lazy<Wtf8Atom> =
38    Lazy::new(|| crate::annotations::ANNOTATION_TRANSITION.into());
39
40/// Changes the chunking type for the annotated import
41static ANNOTATION_CHUNKING_TYPE: Lazy<Wtf8Atom> =
42    Lazy::new(|| crate::annotations::ANNOTATION_CHUNKING_TYPE.into());
43
44/// Changes the type of the resolved module (only "json" is supported currently)
45static ATTRIBUTE_MODULE_TYPE: Lazy<Wtf8Atom> = Lazy::new(|| atom!("type").into());
46
47impl ImportAnnotations {
48    pub fn parse(with: Option<&ObjectLit>) -> ImportAnnotations {
49        let Some(with) = with else {
50            return ImportAnnotations::default();
51        };
52
53        let mut map = BTreeMap::new();
54
55        // The `with` clause is way more restrictive than `ObjectLit`, it only allows
56        // string -> value and value can only be a string.
57        // We just ignore everything else here till the SWC ast is more restrictive.
58        for (key, value) in with.props.iter().filter_map(|prop| {
59            let kv = prop.as_prop()?.as_key_value()?;
60
61            let Lit::Str(str) = kv.value.as_lit()? else {
62                return None;
63            };
64
65            Some((&kv.key, str))
66        }) {
67            let key = match key {
68                PropName::Ident(ident) => ident.sym.clone().into(),
69                PropName::Str(str) => str.value.clone(),
70                // the rest are invalid, ignore for now till SWC ast is correct
71                _ => continue,
72            };
73
74            map.insert(key, value.value.clone());
75        }
76
77        ImportAnnotations { map }
78    }
79
80    pub fn parse_dynamic(with: &JsValue) -> Option<ImportAnnotations> {
81        let mut map = BTreeMap::new();
82
83        let JsValue::Object { parts, .. } = with else {
84            return None;
85        };
86
87        for part in parts.iter() {
88            let ObjectPart::KeyValue(key, value) = part else {
89                continue;
90            };
91            let (
92                JsValue::Constant(ConstantValue::Str(key)),
93                JsValue::Constant(ConstantValue::Str(value)),
94            ) = (key, value)
95            else {
96                continue;
97            };
98
99            map.insert(
100                key.as_atom().into_owned().into(),
101                value.as_atom().into_owned().into(),
102            );
103        }
104
105        Some(ImportAnnotations { map })
106    }
107
108    /// Returns the content on the transition annotation
109    pub fn transition(&self) -> Option<Cow<'_, str>> {
110        self.get(&ANNOTATION_TRANSITION)
111            .map(|v| v.to_string_lossy())
112    }
113
114    /// Returns the content on the chunking-type annotation
115    pub fn chunking_type(&self) -> Option<&Wtf8Atom> {
116        self.get(&ANNOTATION_CHUNKING_TYPE)
117    }
118
119    /// Returns the content on the type attribute
120    pub fn module_type(&self) -> Option<&Wtf8Atom> {
121        self.get(&ATTRIBUTE_MODULE_TYPE)
122    }
123
124    pub fn get(&self, key: &Wtf8Atom) -> Option<&Wtf8Atom> {
125        self.map.get(key)
126    }
127}
128
129impl Display for ImportAnnotations {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        let mut it = self.map.iter();
132        if let Some((k, v)) = it.next() {
133            write!(f, "{{ {}: {}", k.to_string_lossy(), v.to_string_lossy())?
134        } else {
135            return f.write_str("{}");
136        };
137        for (k, v) in it {
138            write!(f, ", {}: {}", k.to_string_lossy(), v.to_string_lossy())?
139        }
140        f.write_str(" }")
141    }
142}
143
144#[derive(Debug)]
145pub(crate) enum Reexport {
146    Star,
147    Namespace { exported: Atom },
148    Named { imported: Atom, exported: Atom },
149}
150
151/// The storage for all kinds of imports.
152///
153/// Note that when it's initialized by calling `analyze`, it only contains ESM
154/// import/exports.
155#[derive(Default, Debug)]
156pub(crate) struct ImportMap {
157    /// Map from identifier to (index in references, exported symbol)
158    imports: FxIndexMap<Id, (usize, Atom)>,
159
160    /// Map from identifier to index in references
161    namespace_imports: FxIndexMap<Id, usize>,
162
163    /// List of (index in references, imported symbol, exported symbol)
164    reexports: Vec<(usize, Reexport)>,
165
166    /// Ordered list of imported symbols
167    references: FxIndexSet<ImportMapReference>,
168
169    /// True, when the module has imports
170    has_imports: bool,
171
172    /// True, when the module has exports
173    has_exports: bool,
174
175    /// True if the module is an ESM module due to top-level await.
176    has_top_level_await: bool,
177
178    /// True if the module has "use strict"
179    pub(crate) strict: bool,
180
181    /// Locations of [webpack-style "magic comments"][magic] that override import behaviors.
182    ///
183    /// Most commonly, these are `/* webpackIgnore: true */` comments. See [ImportAttributes] for
184    /// full details.
185    ///
186    /// [magic]: https://webpack.js.org/api/module-methods/#magic-comments
187    attributes: FxHashMap<BytePos, ImportAttributes>,
188
189    /// The module specifiers of star imports that are accessed dynamically and should be imported
190    /// as a whole.
191    full_star_imports: FxHashSet<Wtf8Atom>,
192
193    pub(crate) exports: FxHashMap<RcStr, Id>,
194}
195
196/// Represents a collection of [webpack-style "magic comments"][magic] that override import
197/// behaviors.
198///
199/// [magic]: https://webpack.js.org/api/module-methods/#magic-comments
200#[derive(Debug)]
201pub struct ImportAttributes {
202    /// Should we ignore this import expression when bundling? If so, the import expression will be
203    /// left as-is in Turbopack's output.
204    ///
205    /// This is set by using either a `webpackIgnore` or `turbopackIgnore` comment.
206    ///
207    /// Example:
208    /// ```js
209    /// const a = import(/* webpackIgnore: true */ "a");
210    /// const b = import(/* turbopackIgnore: true */ "b");
211    /// ```
212    pub ignore: bool,
213}
214
215impl ImportAttributes {
216    pub const fn empty() -> Self {
217        ImportAttributes { ignore: false }
218    }
219
220    pub fn empty_ref() -> &'static Self {
221        // use `Self::empty` here as `Default::default` isn't const
222        static DEFAULT_VALUE: ImportAttributes = ImportAttributes::empty();
223        &DEFAULT_VALUE
224    }
225}
226
227impl Default for ImportAttributes {
228    fn default() -> Self {
229        ImportAttributes::empty()
230    }
231}
232
233impl Default for &ImportAttributes {
234    fn default() -> Self {
235        ImportAttributes::empty_ref()
236    }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Hash)]
240pub(crate) enum ImportedSymbol {
241    ModuleEvaluation,
242    Symbol(Atom),
243    Exports,
244    Part(u32),
245    PartEvaluation(u32),
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Hash)]
249pub(crate) struct ImportMapReference {
250    pub module_path: Wtf8Atom,
251    pub imported_symbol: ImportedSymbol,
252    pub annotations: ImportAnnotations,
253    pub issue_source: Option<IssueSource>,
254}
255
256impl ImportMap {
257    pub fn is_esm(&self, specified_type: SpecifiedModuleType) -> bool {
258        if self.has_exports {
259            return true;
260        }
261
262        match specified_type {
263            SpecifiedModuleType::Automatic => {
264                self.has_exports || self.has_imports || self.has_top_level_await
265            }
266            SpecifiedModuleType::CommonJs => false,
267            SpecifiedModuleType::EcmaScript => true,
268        }
269    }
270
271    pub fn get_import(&self, id: &Id) -> Option<JsValue> {
272        if let Some((i, i_sym)) = self.imports.get(id) {
273            let r = &self.references[*i];
274            return Some(JsValue::member(
275                Box::new(JsValue::Module(ModuleValue {
276                    module: r.module_path.clone(),
277                    annotations: r.annotations.clone(),
278                })),
279                Box::new(i_sym.clone().into()),
280            ));
281        }
282        if let Some(i) = self.namespace_imports.get(id) {
283            let r = &self.references[*i];
284            return Some(JsValue::Module(ModuleValue {
285                module: r.module_path.clone(),
286                annotations: r.annotations.clone(),
287            }));
288        }
289        None
290    }
291
292    pub fn get_attributes(&self, span: Span) -> &ImportAttributes {
293        self.attributes.get(&span.lo).unwrap_or_default()
294    }
295
296    // TODO this could return &str instead of String to avoid cloning
297    pub fn get_binding(&self, id: &Id) -> Option<(usize, Option<RcStr>)> {
298        if let Some((i, i_sym)) = self.imports.get(id) {
299            return Some((*i, Some(i_sym.as_str().into())));
300        }
301        if let Some(i) = self.namespace_imports.get(id) {
302            return Some((*i, None));
303        }
304        None
305    }
306
307    pub fn references(&self) -> impl ExactSizeIterator<Item = &ImportMapReference> {
308        self.references.iter()
309    }
310
311    pub fn reexports(&self) -> impl ExactSizeIterator<Item = (usize, &Reexport)> {
312        self.reexports.iter().map(|(i, r)| (*i, r))
313    }
314
315    /// Analyze ES import
316    pub(super) fn analyze(
317        m: &Program,
318        source: Option<ResolvedVc<Box<dyn Source>>>,
319        comments: Option<&dyn Comments>,
320    ) -> Self {
321        let mut data = ImportMap::default();
322
323        // We have to analyze imports first to determine if a star import is dynamic.
324        // We can't do this in the visitor because import may (and likely) comes before usages, and
325        // a method invoked after visitor will not work because we need to preserve the import
326        // order.
327
328        if let Program::Module(m) = m {
329            let mut candidates = FxIndexMap::default();
330
331            // Imports are hoisted to the top of the module.
332            // So we have to collect all imports first.
333            m.body.iter().for_each(|stmt| {
334                if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = stmt {
335                    for s in &import.specifiers {
336                        if let ImportSpecifier::Namespace(s) = s {
337                            candidates.insert(s.local.to_id(), import.src.value.clone());
338                        }
339                    }
340                }
341            });
342
343            let mut analyzer = StarImportAnalyzer {
344                candidates,
345                full_star_imports: &mut data.full_star_imports,
346            };
347            m.visit_with(&mut analyzer);
348        }
349
350        let mut analyzer = Analyzer {
351            data: &mut data,
352            source,
353            comments,
354        };
355        m.visit_with(&mut analyzer);
356
357        data
358    }
359
360    pub(crate) fn should_import_all(&self, esm_reference_index: usize) -> bool {
361        let r = &self.references[esm_reference_index];
362
363        self.full_star_imports.contains(&r.module_path)
364    }
365}
366
367struct StarImportAnalyzer<'a> {
368    /// The local identifiers of the star imports
369    candidates: FxIndexMap<Id, Wtf8Atom>,
370    full_star_imports: &'a mut FxHashSet<Wtf8Atom>,
371}
372
373impl Visit for StarImportAnalyzer<'_> {
374    fn visit_expr(&mut self, node: &Expr) {
375        if let Expr::Ident(i) = node
376            && let Some(module_path) = self.candidates.get(&i.to_id())
377        {
378            self.full_star_imports.insert(module_path.clone());
379            return;
380        }
381
382        node.visit_children_with(self);
383    }
384
385    fn visit_import_decl(&mut self, _: &ImportDecl) {}
386
387    fn visit_member_expr(&mut self, node: &MemberExpr) {
388        match &node.prop {
389            MemberProp::Ident(..) | MemberProp::PrivateName(..) => {
390                if node.obj.is_ident() {
391                    return;
392                }
393                // We can skip `visit_expr(obj)` because it's not a dynamic access
394                node.obj.visit_children_with(self);
395            }
396            MemberProp::Computed(..) => {
397                node.obj.visit_with(self);
398                node.prop.visit_with(self);
399            }
400        }
401    }
402
403    fn visit_pat(&mut self, pat: &Pat) {
404        if let Pat::Ident(i) = pat
405            && let Some(module_path) = self.candidates.get(&i.to_id())
406        {
407            self.full_star_imports.insert(module_path.clone());
408            return;
409        }
410
411        pat.visit_children_with(self);
412    }
413
414    fn visit_simple_assign_target(&mut self, node: &SimpleAssignTarget) {
415        if let SimpleAssignTarget::Ident(i) = node
416            && let Some(module_path) = self.candidates.get(&i.to_id())
417        {
418            self.full_star_imports.insert(module_path.clone());
419            return;
420        }
421
422        node.visit_children_with(self);
423    }
424}
425
426struct Analyzer<'a> {
427    data: &'a mut ImportMap,
428    source: Option<ResolvedVc<Box<dyn Source>>>,
429    comments: Option<&'a dyn Comments>,
430}
431
432impl Analyzer<'_> {
433    fn ensure_reference(
434        &mut self,
435        span: Span,
436        module_path: Wtf8Atom,
437        imported_symbol: ImportedSymbol,
438        annotations: ImportAnnotations,
439    ) -> Option<usize> {
440        let issue_source = self
441            .source
442            .map(|s| IssueSource::from_swc_offsets(s, span.lo.to_u32(), span.hi.to_u32()));
443
444        let r = ImportMapReference {
445            module_path,
446            imported_symbol,
447            issue_source,
448            annotations,
449        };
450        if let Some(i) = self.data.references.get_index_of(&r) {
451            Some(i)
452        } else {
453            let i = self.data.references.len();
454            self.data.references.insert(r);
455            Some(i)
456        }
457    }
458}
459
460impl Visit for Analyzer<'_> {
461    fn visit_import_decl(&mut self, import: &ImportDecl) {
462        self.data.has_imports = true;
463
464        let annotations = ImportAnnotations::parse(import.with.as_deref());
465
466        let internal_symbol = parse_with(import.with.as_deref());
467
468        if internal_symbol.is_none() {
469            self.ensure_reference(
470                import.span,
471                import.src.value.clone(),
472                ImportedSymbol::ModuleEvaluation,
473                annotations.clone(),
474            );
475        }
476
477        for s in &import.specifiers {
478            let symbol = internal_symbol
479                .clone()
480                .unwrap_or_else(|| get_import_symbol_from_import(s));
481            let i = self.ensure_reference(
482                import.span,
483                import.src.value.clone(),
484                symbol,
485                annotations.clone(),
486            );
487            let i = match i {
488                Some(v) => v,
489                None => continue,
490            };
491
492            let (local, orig_sym) = match s {
493                ImportSpecifier::Named(ImportNamedSpecifier {
494                    local, imported, ..
495                }) => match imported {
496                    Some(imported) => (local.to_id(), imported.atom().into_owned()),
497                    _ => (local.to_id(), local.sym.clone()),
498                },
499                ImportSpecifier::Default(s) => (s.local.to_id(), atom!("default")),
500                ImportSpecifier::Namespace(s) => {
501                    self.data.namespace_imports.insert(s.local.to_id(), i);
502                    continue;
503                }
504            };
505
506            self.data.imports.insert(local, (i, orig_sym));
507        }
508        if import.specifiers.is_empty()
509            && let Some(internal_symbol) = internal_symbol
510        {
511            self.ensure_reference(
512                import.span,
513                import.src.value.clone(),
514                internal_symbol,
515                annotations,
516            );
517        }
518    }
519
520    fn visit_export_all(&mut self, export: &ExportAll) {
521        self.data.has_exports = true;
522
523        let annotations = ImportAnnotations::parse(export.with.as_deref());
524
525        self.ensure_reference(
526            export.span,
527            export.src.value.clone(),
528            ImportedSymbol::ModuleEvaluation,
529            annotations.clone(),
530        );
531        let symbol = parse_with(export.with.as_deref());
532
533        let i = self.ensure_reference(
534            export.span,
535            export.src.value.clone(),
536            symbol.unwrap_or(ImportedSymbol::Exports),
537            annotations,
538        );
539        if let Some(i) = i {
540            self.data.reexports.push((i, Reexport::Star));
541        }
542    }
543
544    fn visit_named_export(&mut self, export: &NamedExport) {
545        self.data.has_exports = true;
546
547        let Some(ref src) = export.src else {
548            export.visit_children_with(self);
549            return;
550        };
551
552        let annotations = ImportAnnotations::parse(export.with.as_deref());
553
554        let internal_symbol = parse_with(export.with.as_deref());
555
556        if internal_symbol.is_none() || export.specifiers.is_empty() {
557            self.ensure_reference(
558                export.span,
559                src.value.clone(),
560                ImportedSymbol::ModuleEvaluation,
561                annotations.clone(),
562            );
563        }
564
565        for spec in export.specifiers.iter() {
566            let symbol = internal_symbol
567                .clone()
568                .unwrap_or_else(|| get_import_symbol_from_export(spec));
569
570            let i =
571                self.ensure_reference(export.span, src.value.clone(), symbol, annotations.clone());
572            let i = match i {
573                Some(v) => v,
574                None => continue,
575            };
576
577            match spec {
578                ExportSpecifier::Namespace(n) => {
579                    self.data.reexports.push((
580                        i,
581                        Reexport::Namespace {
582                            exported: n.name.atom().into_owned(),
583                        },
584                    ));
585                }
586                ExportSpecifier::Default(d) => {
587                    self.data.reexports.push((
588                        i,
589                        Reexport::Named {
590                            imported: atom!("default"),
591                            exported: d.exported.sym.clone(),
592                        },
593                    ));
594                }
595                ExportSpecifier::Named(n) => {
596                    self.data.reexports.push((
597                        i,
598                        Reexport::Named {
599                            imported: n.orig.atom().into_owned(),
600                            exported: n.exported.as_ref().unwrap_or(&n.orig).atom().into_owned(),
601                        },
602                    ));
603                }
604            }
605        }
606    }
607
608    fn visit_export_decl(&mut self, n: &ExportDecl) {
609        self.data.has_exports = true;
610
611        if self.comments.is_some() {
612            // only visit children if we potentially need to mark import / requires
613            n.visit_children_with(self);
614        }
615
616        match &n.decl {
617            Decl::Class(n) => {
618                self.data
619                    .exports
620                    .insert(n.ident.sym.as_str().into(), n.ident.to_id());
621            }
622            Decl::Fn(n) => {
623                self.data
624                    .exports
625                    .insert(n.ident.sym.as_str().into(), n.ident.to_id());
626            }
627            Decl::Var(..) | Decl::Using(..) => {
628                let ids: Vec<Id> = find_pat_ids(&n.decl);
629                for id in ids {
630                    self.data.exports.insert(id.0.as_str().into(), id);
631                }
632            }
633            _ => {}
634        }
635    }
636
637    fn visit_export_default_decl(&mut self, n: &ExportDefaultDecl) {
638        self.data.has_exports = true;
639
640        if self.comments.is_some() {
641            // only visit children if we potentially need to mark import / requires
642            n.visit_children_with(self);
643        }
644
645        self.data.exports.insert(
646            rcstr!("default"),
647            // Mirror what `EsmModuleItem::code_generation` does, these are live bindings if the
648            // class/function has an identifier.
649            match &n.decl {
650                DefaultDecl::Class(ClassExpr { ident, .. })
651                | DefaultDecl::Fn(FnExpr { ident, .. }) => ident.as_ref().map_or_else(
652                    || {
653                        (
654                            magic_identifier::mangle("default export").into(),
655                            SyntaxContext::empty(),
656                        )
657                    },
658                    |ident| ident.to_id(),
659                ),
660                DefaultDecl::TsInterfaceDecl(_) => {
661                    // not matching, might happen due to eventual consistency
662                    (
663                        magic_identifier::mangle("default export").into(),
664                        SyntaxContext::empty(),
665                    )
666                }
667            },
668        );
669    }
670
671    fn visit_export_default_expr(&mut self, n: &ExportDefaultExpr) {
672        self.data.has_exports = true;
673
674        if self.comments.is_some() {
675            // only visit children if we potentially need to mark import / requires
676            n.visit_children_with(self);
677        }
678
679        self.data.exports.insert(
680            rcstr!("default"),
681            (
682                // `EsmModuleItem::code_generation` inserts this variable.
683                magic_identifier::mangle("default export").into(),
684                SyntaxContext::empty(),
685            ),
686        );
687    }
688
689    fn visit_export_named_specifier(&mut self, n: &ExportNamedSpecifier) {
690        let ModuleExportName::Ident(local) = &n.orig else {
691            // This is only possible for re-exports, but they are already handled earlier in
692            // visit_named_export.
693            unreachable!("string reexports should have been already handled in visit_named_export");
694        };
695        let exported = n.exported.as_ref().unwrap_or(&n.orig);
696        self.data
697            .exports
698            .insert(exported.atom().as_str().into(), local.to_id());
699    }
700
701    fn visit_export_default_specifier(&mut self, n: &ExportDefaultSpecifier) {
702        self.data
703            .exports
704            .insert(rcstr!("default"), n.exported.to_id());
705    }
706
707    fn visit_program(&mut self, m: &Program) {
708        self.data.has_top_level_await = has_top_level_await(m).is_some();
709        self.data.strict = match m {
710            Program::Module(module) => module
711                .body
712                .iter()
713                .take_while(|s| s.directive_continue())
714                .any(IsDirective::is_use_strict),
715            Program::Script(script) => script
716                .body
717                .iter()
718                .take_while(|s| s.directive_continue())
719                .any(IsDirective::is_use_strict),
720        };
721
722        m.visit_children_with(self);
723    }
724
725    fn visit_stmt(&mut self, n: &Stmt) {
726        if self.comments.is_some() {
727            // only visit children if we potentially need to mark import / requires
728            n.visit_children_with(self);
729        }
730    }
731
732    /// check if import or require contains an ignore comment
733    ///
734    /// We are checking for the following cases:
735    /// - import(/* webpackIgnore: true */ "a")
736    /// - require(/* webpackIgnore: true */ "a")
737    ///
738    /// We can do this by checking if any of the comment spans are between the
739    /// callee and the first argument.
740    //
741    // potentially support more webpack magic comments in the future:
742    // https://webpack.js.org/api/module-methods/#magic-comments
743    fn visit_call_expr(&mut self, n: &CallExpr) {
744        // we could actually unwrap thanks to the optimisation above but it can't hurt to be safe...
745        if let Some(comments) = self.comments {
746            let callee_span = match &n.callee {
747                Callee::Import(Import { span, .. }) => Some(*span),
748                Callee::Expr(e) => Some(e.span()),
749                _ => None,
750            };
751
752            let ignore_directive = parse_ignore_directive(comments, n.args.first());
753
754            if let Some((callee_span, ignore_directive)) = callee_span.zip(ignore_directive) {
755                self.data.attributes.insert(
756                    callee_span.lo,
757                    ImportAttributes {
758                        ignore: ignore_directive,
759                    },
760                );
761            };
762        }
763
764        n.visit_children_with(self);
765    }
766
767    fn visit_new_expr(&mut self, n: &NewExpr) {
768        // we could actually unwrap thanks to the optimisation above but it can't hurt to be safe...
769        if let Some(comments) = self.comments {
770            let callee_span = match &n.callee {
771                box Expr::Ident(Ident { sym, .. }) if sym == "Worker" => Some(n.span),
772                _ => None,
773            };
774
775            let ignore_directive = parse_ignore_directive(comments, n.args.iter().flatten().next());
776
777            if let Some((callee_span, ignore_directive)) = callee_span.zip(ignore_directive) {
778                self.data.attributes.insert(
779                    callee_span.lo,
780                    ImportAttributes {
781                        ignore: ignore_directive,
782                    },
783                );
784            };
785        }
786
787        n.visit_children_with(self);
788    }
789}
790
791fn parse_ignore_directive(comments: &dyn Comments, value: Option<&ExprOrSpread>) -> Option<bool> {
792    // we are interested here in the last comment with a valid directive
793    value
794        .map(|arg| arg.span_lo())
795        .and_then(|comment_pos| comments.get_leading(comment_pos))
796        .iter()
797        .flatten()
798        .rev()
799        .filter_map(|comment| {
800            let (directive, value) = comment.text.trim().split_once(':')?;
801            // support whitespace between the colon
802            match (directive.trim(), value.trim()) {
803                ("webpackIgnore" | "turbopackIgnore", "true") => Some(true),
804                ("webpackIgnore" | "turbopackIgnore", "false") => Some(false),
805                _ => None, // ignore anything else
806            }
807        })
808        .next()
809}
810
811fn parse_with(with: Option<&ObjectLit>) -> Option<ImportedSymbol> {
812    find_turbopack_part_id_in_asserts(with?).map(|v| match v {
813        PartId::Internal(index, true) => ImportedSymbol::PartEvaluation(index),
814        PartId::Internal(index, false) => ImportedSymbol::Part(index),
815        PartId::ModuleEvaluation => ImportedSymbol::ModuleEvaluation,
816        PartId::Export(e) => ImportedSymbol::Symbol(e.as_str().into()),
817        PartId::Exports => ImportedSymbol::Exports,
818    })
819}
820
821fn get_import_symbol_from_import(specifier: &ImportSpecifier) -> ImportedSymbol {
822    match specifier {
823        ImportSpecifier::Named(ImportNamedSpecifier {
824            local, imported, ..
825        }) => ImportedSymbol::Symbol(match imported {
826            Some(imported) => imported.atom().into_owned(),
827            _ => local.sym.clone(),
828        }),
829        ImportSpecifier::Default(..) => ImportedSymbol::Symbol(atom!("default")),
830        ImportSpecifier::Namespace(..) => ImportedSymbol::Exports,
831    }
832}
833
834fn get_import_symbol_from_export(specifier: &ExportSpecifier) -> ImportedSymbol {
835    match specifier {
836        ExportSpecifier::Named(ExportNamedSpecifier { orig, .. }) => {
837            ImportedSymbol::Symbol(orig.atom().into_owned())
838        }
839        ExportSpecifier::Default(..) => ImportedSymbol::Symbol(atom!("default")),
840        ExportSpecifier::Namespace(..) => ImportedSymbol::Exports,
841    }
842}