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