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