Skip to main content

fallow_graph/resolve/
mod.rs

1//! Import specifier resolution using `oxc_resolver`.
2//!
3//! Orchestrates the resolution pipeline: for every extracted module, resolves all
4//! import specifiers in parallel (via rayon) to an [`ResolveResult`], internal file,
5//! npm package, external file, or unresolvable. The entry point is [`resolve_all_imports`].
6//!
7//! Resolution is split into submodules by import kind:
8//! - `static_imports`: ES `import` declarations
9//! - `dynamic_imports`: `import()` expressions and glob-based dynamic patterns
10//! - `require_imports`: CommonJS `require()` calls
11//! - `re_exports`: `export { x } from './y'` re-export sources
12//! - `upgrades`: post-resolution pass fixing non-deterministic bare specifier results
13//!
14//! Handles tsconfig path aliases (auto-discovered per file), pnpm virtual store paths,
15//! React Native platform extensions, and package.json `exports` subpath resolution with
16//! output-to-source directory fallback.
17
18mod auto_imports;
19mod dynamic_imports;
20pub(crate) mod fallbacks;
21mod path_info;
22mod re_exports;
23mod react_native;
24mod require_imports;
25mod specifier;
26mod static_imports;
27#[cfg(test)]
28mod tests;
29mod types;
30mod upgrades;
31
32pub use auto_imports::{UnreadableAutoImportRead, unreadable_auto_import_reads};
33pub use fallbacks::extract_package_name_from_node_modules_path;
34pub use path_info::{
35    extract_package_name, is_bare_specifier, is_path_alias, is_valid_package_name,
36};
37pub use react_native::{PlatformFamilyKey, has_react_native_plugin, platform_family_key};
38pub use types::{
39    OUTPUT_DIRS, ResolveResult, ResolvedImport, ResolvedModule, ResolvedProject, ResolvedReExport,
40    ResolvedReplacedModuleTarget, ResolvedSourceEdge,
41};
42
43use std::sync::Arc;
44
45use std::path::{Path, PathBuf};
46use std::sync::Mutex;
47
48use rayon::prelude::*;
49use rustc_hash::{FxHashMap, FxHashSet};
50
51use fallow_config::{AutoImportKind, AutoImportRule};
52use fallow_types::discover::{DiscoveredFile, FileId};
53use fallow_types::extract::{ImportInfo, ImportedName, ModuleInfo, SemanticFact};
54use oxc_span::Span;
55
56use auto_imports::synthesize_auto_import_edges;
57use dynamic_imports::{GlobMatcherCache, resolve_dynamic_imports, resolve_dynamic_patterns};
58use re_exports::resolve_re_exports;
59use react_native::{build_condition_names, build_extensions, synthesize_platform_family_edges};
60use require_imports::resolve_require_imports;
61use specifier::{build_resolve_options, create_resolver};
62use static_imports::resolve_static_imports;
63use types::{DenoImportMapEntry, PackageManifestInfo, ResolveContext};
64use upgrades::{ResolvedVitestMockOperation, apply_specifier_upgrades};
65
66/// Inputs used to resolve imports for a complete extracted project.
67pub struct ResolveAllImportsInput<'a> {
68    /// Extracted modules whose imports should be resolved.
69    pub modules: &'a [ModuleInfo],
70    /// Discovered source files indexed by [`FileId`].
71    pub files: &'a [DiscoveredFile],
72    /// Workspace package roots used for package self-resolution.
73    pub workspaces: &'a [fallow_config::WorkspaceInfo],
74    /// Active plugin names that affect extensions and resolver conditions.
75    pub active_plugins: &'a [String],
76    /// Configured TypeScript path alias pairs.
77    pub path_aliases: &'a [(String, String)],
78    /// Auto-import rules that synthesize implicit graph edges.
79    pub auto_imports: &'a [AutoImportRule],
80    /// Additional Sass and SCSS include directories.
81    pub scss_include_paths: &'a [PathBuf],
82    /// Static directory mappings for framework-specific asset resolution.
83    pub static_dir_mappings: &'a [(PathBuf, String)],
84    /// Mounts a framework serves for the whole project; see `ResolveContext`.
85    pub framework_static_dir_mappings: &'a [(PathBuf, String)],
86    /// Project root used for package manifest and relative-path resolution.
87    pub root: &'a Path,
88    /// Extra resolver conditions supplied by configuration.
89    pub extra_conditions: &'a [String],
90}
91
92/// Reusable per-project resolver state: the resolver instances, package
93/// manifests, and workspace canonicalization that do not depend on the specific
94/// modules being resolved.
95///
96/// Building these is the bulk of `resolve_all_imports`'s non-parallel setup cost
97/// (workspace `dunce::canonicalize`, root + workspace `package.json` loads, and
98/// resolver construction). A caller that resolves many small inputs against the
99/// same project (the external-stylesheet scanner resolves dozens of node_modules
100/// stylesheets one file at a time) builds a session once and reuses it across
101/// every resolution instead of rebuilding this state per call.
102pub struct ResolverSession {
103    resolver: oxc_resolver::Resolver,
104    style_resolver: oxc_resolver::Resolver,
105    extensions: Vec<String>,
106    condition_names: Vec<String>,
107    package_manifests: Vec<PackageManifestInfo>,
108    has_deno_import_maps: bool,
109    canonical_ws_roots: Vec<PathBuf>,
110    root_is_canonical: bool,
111}
112
113impl ResolverSession {
114    /// Build the reusable resolver state for `input`'s project context (root,
115    /// workspaces, active plugins, and resolver conditions).
116    ///
117    /// The session is valid for any later [`resolve_all_imports_with_session`]
118    /// call whose input shares that project context; in particular the
119    /// `workspaces` slice must be the same (same entries and order) so workspace
120    /// names line up with the cached `canonical_ws_roots`.
121    #[must_use]
122    pub fn new(input: &ResolveAllImportsInput<'_>) -> Self {
123        let canonical_ws_roots: Vec<PathBuf> = input
124            .workspaces
125            .par_iter()
126            .map(|ws| dunce::canonicalize(&ws.root).unwrap_or_else(|_| ws.root.clone()))
127            .collect();
128        let package_manifests = build_package_manifests(input, &canonical_ws_roots);
129        let has_deno_import_maps = package_manifests
130            .iter()
131            .any(|manifest| !manifest.deno_import_map.is_empty());
132        let root_is_canonical = dunce::canonicalize(input.root).is_ok_and(|c| c == input.root);
133
134        let extensions = build_extensions(input.active_plugins);
135        let condition_names = build_condition_names(input.active_plugins, input.extra_conditions);
136        let resolver = create_resolver(input.root, input.active_plugins, input.extra_conditions);
137        let mut style_conditions = input.extra_conditions.to_vec();
138        style_conditions.push("sass".to_string());
139        style_conditions.push("style".to_string());
140        // Derived from the main resolver so both share one filesystem cache and,
141        // on Yarn PnP projects, one parsed manifest.
142        let style_resolver = resolver.clone_with_options(build_resolve_options(
143            input.root,
144            input.active_plugins,
145            &style_conditions,
146        ));
147
148        Self {
149            resolver,
150            style_resolver,
151            extensions,
152            condition_names,
153            package_manifests,
154            has_deno_import_maps,
155            canonical_ws_roots,
156            root_is_canonical,
157        }
158    }
159}
160
161/// Resolve all imports across all modules in parallel.
162#[must_use]
163pub fn resolve_all_imports(input: &ResolveAllImportsInput<'_>) -> ResolvedProject {
164    let session = ResolverSession::new(input);
165    resolve_all_imports_with_session(input, &session)
166}
167
168/// Resolve all imports for `input`, reusing a prebuilt [`ResolverSession`].
169///
170/// This is the single resolution code path; [`resolve_all_imports`] is the
171/// convenience wrapper that builds a fresh session first. `session` MUST have
172/// been built from an input with the same project context as `input` (same
173/// `root`, `workspaces` slice and order, `active_plugins`, and
174/// `extra_conditions`); only `modules` and `files` may differ.
175#[must_use]
176pub fn resolve_all_imports_with_session(
177    input: &ResolveAllImportsInput<'_>,
178    session: &ResolverSession,
179) -> ResolvedProject {
180    let root_is_canonical = session.root_is_canonical;
181    let workspace_roots = build_workspace_roots(input.workspaces, &session.canonical_ws_roots);
182    let canonical_paths = build_canonical_file_paths(input.files, root_is_canonical);
183    let path_to_id = build_path_to_id(input.files, &canonical_paths, root_is_canonical);
184    let raw_path_to_id: FxHashMap<&Path, FileId> = input
185        .files
186        .iter()
187        .map(|f| (f.path.as_path(), f.id))
188        .collect();
189
190    let file_paths: Vec<&Path> = input.files.iter().map(|f| f.path.as_path()).collect();
191
192    let canonical_fallback = if root_is_canonical {
193        Some(types::CanonicalFallback::new(input.files))
194    } else {
195        None
196    };
197
198    let tsconfig_warned: Mutex<FxHashSet<String>> = Mutex::new(FxHashSet::default());
199    let tsconfig_cache = types::TsconfigCache::default();
200    let canonicalize_cache = types::CanonicalizeCache::default();
201    let glob_matcher_cache = GlobMatcherCache::default();
202
203    let ctx = ResolveContext {
204        resolver: &session.resolver,
205        style_resolver: &session.style_resolver,
206        extensions: &session.extensions,
207        path_to_id: &path_to_id,
208        raw_path_to_id: &raw_path_to_id,
209        workspace_roots: &workspace_roots,
210        package_manifests: &session.package_manifests,
211        has_deno_import_maps: session.has_deno_import_maps,
212        condition_names: &session.condition_names,
213        path_aliases: input.path_aliases,
214        scss_include_paths: input.scss_include_paths,
215        static_dir_mappings: input.static_dir_mappings,
216        framework_static_dir_mappings: input.framework_static_dir_mappings,
217        root: input.root,
218        canonical_fallback: canonical_fallback.as_ref(),
219        tsconfig_warned: &tsconfig_warned,
220        tsconfig_cache: &tsconfig_cache,
221        canonicalize_cache: &canonicalize_cache,
222    };
223
224    let resolved_outputs: Vec<ResolvedModuleOutput> = input
225        .modules
226        .par_iter()
227        .filter_map(|module| {
228            resolve_module_imports(
229                module,
230                &ctx,
231                &glob_matcher_cache,
232                &file_paths,
233                &canonical_paths,
234                input.files,
235            )
236        })
237        .collect();
238    let mut resolved = Vec::with_capacity(resolved_outputs.len());
239    let mut vitest_mock_operations = Vec::new();
240    for output in resolved_outputs {
241        resolved.push(output.module);
242        vitest_mock_operations.extend(output.vitest_mock_operations);
243    }
244
245    apply_specifier_upgrades(&mut resolved, &mut vitest_mock_operations);
246
247    synthesize_platform_family_edges(&mut resolved, input.files, input.active_plugins);
248
249    synthesize_auto_import_edges(
250        &mut resolved,
251        input.modules,
252        input.auto_imports,
253        &path_to_id,
254        &raw_path_to_id,
255    );
256
257    let replaced_module_targets = final_replaced_module_targets(vitest_mock_operations);
258
259    ResolvedProject {
260        modules: resolved,
261        replaced_module_targets,
262    }
263}
264
265fn build_workspace_roots<'a>(
266    workspaces: &'a [fallow_config::WorkspaceInfo],
267    canonical_ws_roots: &'a [PathBuf],
268) -> FxHashMap<&'a str, &'a Path> {
269    workspaces
270        .iter()
271        .zip(canonical_ws_roots.iter())
272        .map(|(ws, canonical)| (ws.name.as_str(), canonical.as_path()))
273        .collect()
274}
275
276fn build_canonical_file_paths(files: &[DiscoveredFile], root_is_canonical: bool) -> Vec<PathBuf> {
277    if root_is_canonical {
278        return Vec::new();
279    }
280
281    files
282        .par_iter()
283        .map(|f| dunce::canonicalize(&f.path).unwrap_or_else(|_| f.path.clone()))
284        .collect()
285}
286
287/// Load the root package manifest plus each workspace manifest into the
288/// `PackageManifestInfo` list used for `exports` / `imports` resolution.
289///
290/// Manifests come from `package.json` when present, otherwise `deno.json` /
291/// `deno.jsonc` (`name` + `exports`), so pure Deno workspaces resolve without
292/// npm bridge files.
293fn build_package_manifests(
294    input: &ResolveAllImportsInput<'_>,
295    canonical_ws_roots: &[PathBuf],
296) -> Vec<PackageManifestInfo> {
297    let root_canonical =
298        dunce::canonicalize(input.root).unwrap_or_else(|_| input.root.to_path_buf());
299    let root_probe = fallow_config::probe_dir_manifest(input.root);
300    let root_import_map = merge_deno_import_maps(&[], input.root, root_probe.deno_import_map);
301    let mut package_manifests = Vec::new();
302    if let Ok(Some((_name, package_json, _deps))) = root_probe.manifest {
303        package_manifests.push(PackageManifestInfo {
304            root: input.root.to_path_buf(),
305            canonical_root: root_canonical,
306            name: package_json.name.clone(),
307            package_json,
308            deno_import_map: root_import_map.clone(),
309        });
310    }
311    for (ws, canonical_root) in input.workspaces.iter().zip(canonical_ws_roots.iter()) {
312        let ws_probe = fallow_config::probe_dir_manifest(&ws.root);
313        if let Ok(Some((_name, package_json, _deps))) = ws_probe.manifest {
314            package_manifests.push(PackageManifestInfo {
315                root: ws.root.clone(),
316                canonical_root: canonical_root.clone(),
317                name: package_json.name.clone().or_else(|| Some(ws.name.clone())),
318                package_json,
319                deno_import_map: merge_deno_import_maps(
320                    &root_import_map,
321                    &ws.root,
322                    ws_probe.deno_import_map,
323                ),
324            });
325        }
326    }
327    package_manifests
328}
329
330/// Merge inherited and package-local Deno import maps once per resolver
331/// session. Equal local keys replace inherited keys while retaining the
332/// declaring directory needed for relative target resolution. The local map
333/// comes from the caller's single-probe manifest load so the Deno config is
334/// not read and parsed a second time.
335fn merge_deno_import_maps(
336    inherited: &[DenoImportMapEntry],
337    package_root: &Path,
338    local: Option<(PathBuf, Vec<(String, String)>)>,
339) -> Vec<DenoImportMapEntry> {
340    let mut entries: FxHashMap<String, DenoImportMapEntry> = inherited
341        .iter()
342        .cloned()
343        .map(|entry| (entry.key.clone(), entry))
344        .collect();
345
346    if let Some((config_path, local_entries)) = local {
347        let declaring_dir = config_path.parent().unwrap_or(package_root).to_path_buf();
348        for (key, target) in local_entries {
349            entries.insert(
350                key.clone(),
351                DenoImportMapEntry {
352                    key,
353                    target,
354                    declaring_dir: declaring_dir.clone(),
355                },
356            );
357        }
358    }
359
360    let mut entries: Vec<_> = entries.into_values().collect();
361    entries.sort_by(|a, b| a.key.cmp(&b.key));
362    entries
363}
364
365/// Build the path-to-`FileId` index, keyed by canonical paths when the root is
366/// not already canonical and by raw paths otherwise.
367fn build_path_to_id<'a>(
368    files: &'a [DiscoveredFile],
369    canonical_paths: &'a [PathBuf],
370    root_is_canonical: bool,
371) -> FxHashMap<&'a Path, FileId> {
372    if root_is_canonical {
373        files.iter().map(|f| (f.path.as_path(), f.id)).collect()
374    } else {
375        canonical_paths
376            .iter()
377            .enumerate()
378            .map(|(idx, canonical)| (canonical.as_path(), files[idx].id))
379            .collect()
380    }
381}
382
383fn resolve_module_imports(
384    module: &ModuleInfo,
385    ctx: &ResolveContext<'_>,
386    glob_matcher_cache: &GlobMatcherCache,
387    file_paths: &[&Path],
388    canonical_paths: &[PathBuf],
389    files: &[DiscoveredFile],
390) -> Option<ResolvedModuleOutput> {
391    let Some(file_path) = file_paths.get(module.file_id.0 as usize) else {
392        tracing::warn!(
393            file_id = module.file_id.0,
394            "Skipping module with unknown file_id during resolution"
395        );
396        return None;
397    };
398
399    let mut all_imports = resolve_static_imports(ctx, file_path, &module.imports);
400    all_imports.extend(resolve_require_imports(
401        ctx,
402        file_path,
403        &module.require_calls,
404    ));
405
406    let from_dir = if canonical_paths.is_empty() {
407        file_path.parent().unwrap_or(file_path)
408    } else {
409        canonical_paths
410            .get(module.file_id.0 as usize)
411            .and_then(|p| p.parent())
412            .unwrap_or(file_path)
413    };
414
415    let vitest_mock_operations =
416        resolve_vitest_mock_operations(module.file_id, &module.semantic_facts, ctx, file_path);
417    let module = build_resolved_module(ResolvedModuleBuildInput {
418        module,
419        ctx,
420        glob_matcher_cache,
421        file_path,
422        from_dir,
423        canonical_paths,
424        files,
425        all_imports,
426    });
427
428    Some(ResolvedModuleOutput {
429        module,
430        vitest_mock_operations,
431    })
432}
433
434struct ResolvedModuleOutput {
435    module: ResolvedModule,
436    vitest_mock_operations: Vec<ResolvedVitestMockOperation>,
437}
438
439fn resolve_vitest_mock_operations(
440    source_file: FileId,
441    semantic_facts: &[SemanticFact],
442    ctx: &ResolveContext<'_>,
443    file_path: &Path,
444) -> Vec<ResolvedVitestMockOperation> {
445    let mut operations: Vec<_> = semantic_facts
446        .iter()
447        .filter_map(|fact| {
448            let SemanticFact::VitestModuleMockOperation(operation) = fact else {
449                return None;
450            };
451            Some(ResolvedVitestMockOperation {
452                source_file,
453                source_specifier: operation.source.clone(),
454                call_start: operation.call_start,
455                action: operation.action,
456                target: specifier::resolve_specifier(ctx, file_path, &operation.source, false),
457            })
458        })
459        .collect();
460    operations.sort_unstable_by(|left, right| {
461        left.call_start
462            .cmp(&right.call_start)
463            .then_with(|| left.source_specifier.cmp(&right.source_specifier))
464    });
465    operations
466}
467
468fn final_replaced_module_targets(
469    mut operations: Vec<ResolvedVitestMockOperation>,
470) -> Vec<ResolvedReplacedModuleTarget> {
471    operations.sort_unstable_by(|left, right| {
472        left.source_file
473            .0
474            .cmp(&right.source_file.0)
475            .then_with(|| left.call_start.cmp(&right.call_start))
476            .then_with(|| left.source_specifier.cmp(&right.source_specifier))
477    });
478
479    let mut final_operations = FxHashMap::default();
480    for operation in operations {
481        let Some(target_file) = operation.target.internal_file_id() else {
482            continue;
483        };
484        final_operations.insert((operation.source_file, target_file), operation.action);
485    }
486
487    let mut targets: Vec<_> = final_operations
488        .into_iter()
489        .filter_map(|((source_file, target_file), action)| {
490            action
491                .replaces_original()
492                .then_some(ResolvedReplacedModuleTarget {
493                    source_file,
494                    target_file,
495                })
496        })
497        .collect();
498    targets.sort_unstable_by_key(|target| (target.source_file.0, target.target_file.0));
499    targets
500}
501
502struct ResolvedModuleBuildInput<'a> {
503    module: &'a ModuleInfo,
504    ctx: &'a ResolveContext<'a>,
505    glob_matcher_cache: &'a GlobMatcherCache,
506    file_path: &'a Path,
507    from_dir: &'a Path,
508    canonical_paths: &'a [PathBuf],
509    files: &'a [DiscoveredFile],
510    all_imports: Vec<types::ResolvedImport>,
511}
512
513fn build_resolved_module(input: ResolvedModuleBuildInput<'_>) -> ResolvedModule {
514    ResolvedModule {
515        file_id: input.module.file_id,
516        path: input.file_path.to_path_buf(),
517        exports: Arc::clone(&input.module.exports),
518        re_exports: resolve_re_exports(input.ctx, input.file_path, &input.module.re_exports),
519        resolved_imports: input.all_imports,
520        resolved_dynamic_imports: resolve_dynamic_imports(
521            input.ctx,
522            input.file_path,
523            &input.module.dynamic_imports,
524        ),
525        resolved_dynamic_patterns: resolve_dynamic_patterns(
526            input.glob_matcher_cache,
527            input.from_dir,
528            &input.module.dynamic_import_patterns,
529            input.canonical_paths,
530            input.files,
531        ),
532        member_accesses: Arc::clone(&input.module.member_accesses),
533        semantic_facts: Arc::clone(&input.module.semantic_facts),
534        whole_object_uses: Arc::clone(&input.module.whole_object_uses),
535        has_cjs_exports: input.module.has_cjs_exports,
536        has_angular_component_template_url: input.module.has_angular_component_template_url,
537        unused_import_bindings: input
538            .module
539            .unused_import_bindings
540            .iter()
541            .cloned()
542            .collect(),
543        type_referenced_import_bindings: input.module.type_referenced_import_bindings.clone(),
544        value_referenced_import_bindings: input.module.value_referenced_import_bindings.clone(),
545        namespace_object_aliases: input.module.namespace_object_aliases.clone(),
546        exported_factory_returns: Arc::clone(&input.module.exported_factory_returns),
547        exported_factory_return_object_shapes: Arc::clone(
548            &input.module.exported_factory_return_object_shapes,
549        ),
550        type_member_types: Arc::clone(&input.module.type_member_types),
551    }
552}
553
554fn is_auto_import_builtin(name: &str) -> bool {
555    is_js_auto_import_builtin(name)
556        || is_vue_auto_import_builtin(name)
557        || is_nuxt_auto_import_builtin(name)
558}
559
560fn is_js_auto_import_builtin(name: &str) -> bool {
561    matches!(
562        name,
563        "AbortController"
564            | "AbortSignal"
565            | "Array"
566            | "ArrayBuffer"
567            | "BigInt"
568            | "Blob"
569            | "Boolean"
570            | "Buffer"
571            | "CSS"
572            | "DOMParser"
573            | "Date"
574            | "Document"
575            | "Error"
576            | "Event"
577            | "EventTarget"
578            | "File"
579            | "FormData"
580            | "Intl"
581            | "JSON"
582            | "Map"
583            | "Math"
584            | "Number"
585            | "Object"
586            | "Promise"
587            | "Reflect"
588            | "RegExp"
589            | "Response"
590            | "Set"
591            | "String"
592            | "Symbol"
593            | "URL"
594            | "URLSearchParams"
595            | "WeakMap"
596            | "WeakSet"
597            | "Window"
598            | "alert"
599            | "clearInterval"
600            | "clearTimeout"
601            | "console"
602            | "document"
603            | "fetch"
604            | "global"
605            | "globalThis"
606            | "localStorage"
607            | "navigator"
608            | "process"
609            | "requestAnimationFrame"
610            | "sessionStorage"
611            | "setInterval"
612            | "setTimeout"
613            | "window"
614    )
615}
616
617fn is_vue_auto_import_builtin(name: &str) -> bool {
618    matches!(name, |"computed"| "customRef"
619        | "defineAsyncComponent"
620        | "defineComponent"
621        | "effectScope"
622        | "getCurrentInstance"
623        | "h"
624        | "inject"
625        | "isProxy"
626        | "isReactive"
627        | "isReadonly"
628        | "isRef"
629        | "markRaw"
630        | "nextTick"
631        | "onActivated"
632        | "onBeforeMount"
633        | "onBeforeUnmount"
634        | "onBeforeUpdate"
635        | "onDeactivated"
636        | "onErrorCaptured"
637        | "onMounted"
638        | "onRenderTracked"
639        | "onRenderTriggered"
640        | "onScopeDispose"
641        | "onServerPrefetch"
642        | "onUnmounted"
643        | "onUpdated"
644        | "provide"
645        | "reactive"
646        | "readonly"
647        | "ref"
648        | "resolveComponent"
649        | "shallowReactive"
650        | "shallowReadonly"
651        | "shallowRef"
652        | "toRaw"
653        | "toRef"
654        | "toRefs"
655        | "triggerRef"
656        | "unref"
657        | "watch"
658        | "watchEffect"
659        | "watchPostEffect"
660        | "watchSyncEffect")
661}
662
663fn is_nuxt_auto_import_builtin(name: &str) -> bool {
664    matches!(name, |"useAsyncData"| "useCookie"
665        | "useError"
666        | "useFetch"
667        | "useHead"
668        | "useLazyAsyncData"
669        | "useLazyFetch"
670        | "useNuxtApp"
671        | "useRequestEvent"
672        | "useRequestHeaders"
673        | "useRoute"
674        | "useRouter"
675        | "useRuntimeConfig"
676        | "useSeoMeta"
677        | "useState")
678}
679
680/// Build a synthetic [`ImportInfo`] for a convention auto-import. Component and
681/// default kinds credit the default export; named kinds credit the named export.
682fn synthetic_auto_import_info(name: &str, kind: AutoImportKind) -> ImportInfo {
683    let imported_name = match kind {
684        AutoImportKind::Named => ImportedName::Named(name.to_string()),
685        AutoImportKind::Default | AutoImportKind::DefaultComponent => ImportedName::Default,
686    };
687    ImportInfo {
688        source: format!("<auto-import:{name}>"),
689        imported_name,
690        local_name: name.to_string(),
691        is_type_only: false,
692        is_type_only_star: false,
693        from_style: false,
694        span: Span::default(),
695        source_span: Span::default(),
696    }
697}