turbopack_ecmascript/analyzer/
imports.rs

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