mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
fix: scope Nuxt auto-import resolution
This commit is contained in:
parent
1bc4cd4586
commit
83b1246e45
16 changed files with 466 additions and 73 deletions
|
|
@ -14,8 +14,9 @@
|
|||
* `server/middleware/` file. These are not included in `imports.d.ts`
|
||||
* because they are server-only.
|
||||
*
|
||||
* Both sources produce the same `NuxtAutoImportEntry` shape so the edge-
|
||||
* emission pass can treat them uniformly.
|
||||
* The two sources are kept in separate maps because their visibility scopes
|
||||
* differ: app/client files do not see `server/utils`, while Nitro server
|
||||
* handlers prefer same-named server utilities over client composables.
|
||||
*
|
||||
* Detection: if `.nuxt/imports.d.ts` is absent the repo is not a Nuxt project
|
||||
* and this module returns null immediately, adding zero overhead to non-Nuxt
|
||||
|
|
@ -23,9 +24,8 @@
|
|||
* detection succeeds, so it also costs nothing for non-Nuxt repos.
|
||||
*
|
||||
* Limitations:
|
||||
* - Calls inside string literals or single-line comments are not excluded
|
||||
* by the content scanner. False-positive edges are tagged with
|
||||
* confidence 0.75 to signal that they are heuristic, not type-checked.
|
||||
* - Auto-import edges are still heuristic rather than type-checked. The
|
||||
* emitter tags them with confidence 0.75.
|
||||
* - Re-exports with non-matching aliases (e.g. `flatUnwrap as unwrapSlot`
|
||||
* where `flatUnwrap` is the graph node name) try both the original export
|
||||
* name and the local alias when looking up the graph node.
|
||||
|
|
@ -38,6 +38,8 @@ import path from 'path';
|
|||
|
||||
// ---- types ------------------------------------------------------------------
|
||||
|
||||
export type NuxtAutoImportScope = 'client' | 'server';
|
||||
|
||||
/** A single auto-imported symbol and where it lives in the repo. */
|
||||
export interface NuxtAutoImportEntry {
|
||||
/** The name used at call sites (the alias when `export { X as Y }` form). */
|
||||
|
|
@ -46,16 +48,37 @@ export interface NuxtAutoImportEntry {
|
|||
readonly exportName: string;
|
||||
/** Repo-relative POSIX path to the source file (with extension). */
|
||||
readonly sourceFile: string;
|
||||
/** Which Nuxt/Nitro visibility scope supplied this entry. */
|
||||
readonly scope: NuxtAutoImportScope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregated auto-import map for one Nuxt workspace, keyed by the local
|
||||
* name used in calling code.
|
||||
* Aggregated auto-import maps for one Nuxt workspace, keyed by the local
|
||||
* name used in calling code. Client/shared entries come from
|
||||
* `.nuxt/imports.d.ts`; server entries come from `server/utils`.
|
||||
*/
|
||||
export interface NuxtAutoImportConfig {
|
||||
readonly byLocalName: ReadonlyMap<string, NuxtAutoImportEntry>;
|
||||
readonly clientByLocalName: ReadonlyMap<string, NuxtAutoImportEntry>;
|
||||
readonly serverByLocalName: ReadonlyMap<string, NuxtAutoImportEntry>;
|
||||
}
|
||||
|
||||
const FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'] as const;
|
||||
const GENERATED_DIR_NAMES = new Set([
|
||||
'.git',
|
||||
'.nuxt',
|
||||
'.output',
|
||||
'.next',
|
||||
'build',
|
||||
'coverage',
|
||||
'dist',
|
||||
'node_modules',
|
||||
]);
|
||||
const IMPORTS_DTS_EXPORT_RE = /^export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/gm;
|
||||
const NITRO_DECLARATION_EXPORT_RE =
|
||||
/^export\s+(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)|^export\s+(?:default\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)/gm;
|
||||
const NITRO_VARIABLE_EXPORT_RE = /^export\s+(?:const|let|var)\s+([^;\n]+)/gm;
|
||||
const VARIABLE_DECLARATOR_RE = /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::[^=,]+)?=/g;
|
||||
|
||||
// ---- loader -----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
|
@ -72,18 +95,44 @@ export interface NuxtAutoImportConfig {
|
|||
* are not Nuxt projects.
|
||||
*/
|
||||
export async function loadNuxtAutoImports(repoRoot: string): Promise<NuxtAutoImportConfig | null> {
|
||||
const byLocalName = new Map<string, NuxtAutoImportEntry>();
|
||||
const clientByLocalName = new Map<string, NuxtAutoImportEntry>();
|
||||
const serverByLocalName = new Map<string, NuxtAutoImportEntry>();
|
||||
|
||||
const nuxtInitialized = await collectImportsDts(repoRoot, byLocalName);
|
||||
const nuxtInitialized = await collectImportsDts(repoRoot, clientByLocalName);
|
||||
|
||||
// Only scan server/utils when imports.d.ts was present, confirming this is
|
||||
// an initialized Nuxt project. Without this gate, a non-Nuxt repo with a
|
||||
// server/utils directory would get spurious Nitro auto-import edges.
|
||||
if (nuxtInitialized) {
|
||||
await collectNitroServerUtils(repoRoot, byLocalName);
|
||||
await collectNitroServerUtils(repoRoot, serverByLocalName);
|
||||
}
|
||||
|
||||
return byLocalName.size > 0 ? { byLocalName } : null;
|
||||
const config = { clientByLocalName, serverByLocalName };
|
||||
return hasNuxtAutoImports(config) ? config : null;
|
||||
}
|
||||
|
||||
export function hasNuxtAutoImports(config: NuxtAutoImportConfig): boolean {
|
||||
return config.clientByLocalName.size > 0 || config.serverByLocalName.size > 0;
|
||||
}
|
||||
|
||||
export function getNuxtAutoImportEntry(
|
||||
config: NuxtAutoImportConfig,
|
||||
localName: string,
|
||||
callerFile: string,
|
||||
): NuxtAutoImportEntry | undefined {
|
||||
if (isNitroServerRuntimeFile(callerFile)) {
|
||||
return config.serverByLocalName.get(localName) ?? config.clientByLocalName.get(localName);
|
||||
}
|
||||
return config.clientByLocalName.get(localName);
|
||||
}
|
||||
|
||||
export function isNitroServerRuntimeFile(filePath: string): boolean {
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
return (
|
||||
normalized.startsWith('server/api/') ||
|
||||
normalized.startsWith('server/routes/') ||
|
||||
normalized.startsWith('server/middleware/')
|
||||
);
|
||||
}
|
||||
|
||||
// ---- .nuxt/imports.d.ts -----------------------------------------------------
|
||||
|
|
@ -113,11 +162,10 @@ async function collectImportsDts(
|
|||
}
|
||||
|
||||
const nuxtDir = path.join(repoRoot, '.nuxt');
|
||||
// Matches: export { name1, name2 as alias } from 'source'
|
||||
const lineRe = /^export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/gm;
|
||||
IMPORTS_DTS_EXPORT_RE.lastIndex = 0;
|
||||
let m: RegExpExecArray | null;
|
||||
|
||||
while ((m = lineRe.exec(content)) !== null) {
|
||||
while ((m = IMPORTS_DTS_EXPORT_RE.exec(content)) !== null) {
|
||||
const symbolsRaw = m[1]!;
|
||||
const source = m[2]!;
|
||||
|
||||
|
|
@ -139,7 +187,7 @@ async function collectImportsDts(
|
|||
if (!localName || !exportName) continue;
|
||||
|
||||
if (!byLocalName.has(localName)) {
|
||||
byLocalName.set(localName, { localName, exportName, sourceFile });
|
||||
byLocalName.set(localName, { localName, exportName, sourceFile, scope: 'client' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -172,17 +220,9 @@ async function collectNitroServerUtils(
|
|||
|
||||
const sourceFile = toRepoPosix(repoRoot, absPath);
|
||||
|
||||
// Match top-level exported functions and constants.
|
||||
// Handles: export function X, export async function X, export const X =
|
||||
// Does NOT attempt to match re-exports from other modules.
|
||||
const exportRe =
|
||||
/^export\s+(?:async\s+)?function\s+(\w+)|^export\s+const\s+(\w+)\s*[=:]/gm;
|
||||
let m: RegExpExecArray | null;
|
||||
|
||||
while ((m = exportRe.exec(content)) !== null) {
|
||||
const name = (m[1] ?? m[2])!;
|
||||
for (const name of extractNitroExportNames(content)) {
|
||||
if (!byLocalName.has(name)) {
|
||||
byLocalName.set(name, { localName: name, exportName: name, sourceFile });
|
||||
byLocalName.set(name, { localName: name, exportName: name, sourceFile, scope: 'server' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -197,16 +237,25 @@ function isProjectLocalPath(source: string): boolean {
|
|||
}
|
||||
|
||||
async function resolveExtension(base: string): Promise<string | null> {
|
||||
for (const ext of ['.ts', '.tsx', '.js', '.jsx', '']) {
|
||||
const candidate = ext ? base + ext : base;
|
||||
try {
|
||||
await fs.access(candidate);
|
||||
return candidate;
|
||||
} catch {
|
||||
// try next
|
||||
}
|
||||
const directFile = await firstExistingFile([...FILE_EXTENSIONS.map((ext) => base + ext), base]);
|
||||
if (directFile !== null) return directFile;
|
||||
return firstExistingFile(FILE_EXTENSIONS.map((ext) => path.join(base, `index${ext}`)));
|
||||
}
|
||||
|
||||
async function firstExistingFile(candidates: readonly string[]): Promise<string | null> {
|
||||
const matches = await Promise.all(
|
||||
candidates.map(async (candidate) => ((await isFile(candidate)) ? candidate : null)),
|
||||
);
|
||||
return matches.find((candidate): candidate is string => candidate !== null) ?? null;
|
||||
}
|
||||
|
||||
async function isFile(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
return stat.isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function toRepoPosix(repoRoot: string, absPath: string): string {
|
||||
|
|
@ -232,6 +281,7 @@ async function collectTsFiles(dir: string): Promise<string[]> {
|
|||
return results;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && GENERATED_DIR_NAMES.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...(await collectTsFiles(full)));
|
||||
|
|
@ -241,3 +291,27 @@ async function collectTsFiles(dir: string): Promise<string[]> {
|
|||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function extractNitroExportNames(content: string): string[] {
|
||||
const names = new Set<string>();
|
||||
|
||||
NITRO_DECLARATION_EXPORT_RE.lastIndex = 0;
|
||||
let declaration: RegExpExecArray | null;
|
||||
while ((declaration = NITRO_DECLARATION_EXPORT_RE.exec(content)) !== null) {
|
||||
const name = declaration[1] ?? declaration[2];
|
||||
if (name) names.add(name);
|
||||
}
|
||||
|
||||
NITRO_VARIABLE_EXPORT_RE.lastIndex = 0;
|
||||
let variableDeclaration: RegExpExecArray | null;
|
||||
while ((variableDeclaration = NITRO_VARIABLE_EXPORT_RE.exec(content)) !== null) {
|
||||
const declarationText = variableDeclaration[1]!;
|
||||
VARIABLE_DECLARATOR_RE.lastIndex = 0;
|
||||
let declarator: RegExpExecArray | null;
|
||||
while ((declarator = VARIABLE_DECLARATOR_RE.exec(declarationText)) !== null) {
|
||||
names.add(declarator[1]!);
|
||||
}
|
||||
}
|
||||
|
||||
return [...names];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,14 @@
|
|||
* ./query.ts (TYPESCRIPT_SCOPE_QUERY constant).
|
||||
*/
|
||||
|
||||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
import type { ParsedFile, ScopeId } from 'gitnexus-shared';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { generateId } from '../../../../lib/utils.js';
|
||||
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
||||
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
||||
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { simpleKey } from '../../scope-resolution/graph-bridge/node-lookup.js';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import { typescriptProvider } from '../typescript.js';
|
||||
import { loadTsconfigPaths, type TsconfigPaths } from '../../language-config.js';
|
||||
import { buildSuffixIndex, type SuffixIndex } from '../../import-resolvers/utils.js';
|
||||
|
|
@ -28,7 +29,12 @@ import {
|
|||
resolveTsTarget,
|
||||
type TsResolveContext,
|
||||
} from './index.js';
|
||||
import { loadNuxtAutoImports, type NuxtAutoImportConfig } from './nuxt-auto-imports.js';
|
||||
import {
|
||||
getNuxtAutoImportEntry,
|
||||
hasNuxtAutoImports,
|
||||
loadNuxtAutoImports,
|
||||
type NuxtAutoImportConfig,
|
||||
} from './nuxt-auto-imports.js';
|
||||
|
||||
/** Shape the orchestrator threads in via `RunScopeResolutionInput.resolutionConfig`. */
|
||||
interface TypescriptResolutionConfig {
|
||||
|
|
@ -169,71 +175,66 @@ const typescriptScopeResolver: ScopeResolver = {
|
|||
* For each TypeScript file the hook:
|
||||
* 1. Builds the set of files already explicitly imported (to avoid
|
||||
* creating duplicate edges for symbols imported conventionally).
|
||||
* 2. Scans the raw source for identifier call-patterns (`name(`) and
|
||||
* checks each against the auto-import map.
|
||||
* 3. For each hit that is not already explicitly imported, emits a CALLS
|
||||
* edge from the file's File node to the target function node, and an
|
||||
* IMPORTS edge from the caller file to the source file (once per pair).
|
||||
* 2. Iterates parsed free-call reference sites and checks each against the
|
||||
* auto-import map selected for the caller's Nuxt/Nitro scope.
|
||||
* 3. For each hit that is not shadowed by a local binding or explicit
|
||||
* import, emits a CALLS edge from the file's File node to the target
|
||||
* function node, and an IMPORTS edge from the caller file to the source
|
||||
* file (once per pair).
|
||||
*
|
||||
* Confidence is 0.75 (below the 0.9 used for fully resolved edges) to
|
||||
* signal that these edges are heuristic: the content scanner does not
|
||||
* filter string literals or comments.
|
||||
* signal that these edges are heuristic rather than type-checked.
|
||||
*/
|
||||
emitPostResolutionEdges(graph, parsedFiles, nodeLookup, indexes, ctx) {
|
||||
const cfg = ctx.resolutionConfig as TypescriptResolutionConfig | undefined;
|
||||
const autoImports = cfg?.nuxtAutoImports;
|
||||
if (!autoImports || autoImports.byLocalName.size === 0) return;
|
||||
if (!autoImports || !hasNuxtAutoImports(autoImports)) return;
|
||||
|
||||
// Pre-index: localName -> entry for fast lookup during content scan.
|
||||
const { byLocalName } = autoImports;
|
||||
|
||||
// Pre-build a file -> explicit-import-targets index so the per-file
|
||||
// lookup below is O(1) rather than scanning all import edges every iteration.
|
||||
const explicitImportsByFile = new Map<string, Set<string>>();
|
||||
// Pre-build a file -> explicit imported local names index so importing one
|
||||
// symbol from a source does not suppress other auto-imported symbols from it.
|
||||
const explicitImportNamesByFile = new Map<string, Set<string>>();
|
||||
for (const [scopeId, edges] of indexes.imports) {
|
||||
const scope = indexes.scopeTree.getScope(scopeId);
|
||||
if (!scope?.filePath) continue;
|
||||
let targets = explicitImportsByFile.get(scope.filePath);
|
||||
if (!targets) {
|
||||
targets = new Set<string>();
|
||||
explicitImportsByFile.set(scope.filePath, targets);
|
||||
let names = explicitImportNamesByFile.get(scope.filePath);
|
||||
if (!names) {
|
||||
names = new Set<string>();
|
||||
explicitImportNamesByFile.set(scope.filePath, names);
|
||||
}
|
||||
for (const edge of edges) {
|
||||
if (edge.targetFile !== null) targets.add(edge.targetFile);
|
||||
if (edge.targetFile !== null) names.add(edge.localName);
|
||||
}
|
||||
}
|
||||
|
||||
// Regex matches bare identifier call sites: word-boundary + name + "(".
|
||||
// Excludes `new X(` (constructor calls are not free-function auto-imports).
|
||||
const CALL_RE = /(?<![.\w])([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
|
||||
|
||||
for (const parsedFile of parsedFiles) {
|
||||
const { filePath } = parsedFile;
|
||||
if (!filePath.endsWith('.ts') && !filePath.endsWith('.tsx')) continue;
|
||||
|
||||
const content = ctx.fileContents.get(filePath);
|
||||
if (!content) continue;
|
||||
|
||||
const explicitImports = explicitImportsByFile.get(filePath) ?? new Set<string>();
|
||||
|
||||
const fileId = generateId('File', filePath);
|
||||
const explicitImports = explicitImportNamesByFile.get(filePath) ?? new Set<string>();
|
||||
// Track (sourceFile) pairs already handled for this caller to avoid
|
||||
// emitting duplicate IMPORTS edges and duplicate CALLS edges per symbol.
|
||||
const emittedImports = new Set<string>();
|
||||
const emittedCalls = new Set<string>();
|
||||
|
||||
CALL_RE.lastIndex = 0;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = CALL_RE.exec(content)) !== null) {
|
||||
const localName = m[1]!;
|
||||
const entry = byLocalName.get(localName);
|
||||
for (const site of parsedFile.referenceSites) {
|
||||
if (site.kind !== 'call' || site.callForm !== 'free') continue;
|
||||
|
||||
const localName = site.name;
|
||||
const entry = getNuxtAutoImportEntry(autoImports, localName, filePath);
|
||||
if (!entry) continue;
|
||||
|
||||
const { exportName, sourceFile } = entry;
|
||||
|
||||
// Skip when the file already has an explicit import from this source,
|
||||
// or when the file IS the source (a file cannot auto-import itself).
|
||||
if (explicitImports.has(sourceFile) || sourceFile === filePath) continue;
|
||||
// Skip when the file already binds this name explicitly, when the file
|
||||
// IS the source, or when a lexical same-file binding shadows it.
|
||||
if (
|
||||
explicitImports.has(localName) ||
|
||||
sourceFile === filePath ||
|
||||
hasLocalBindingInScopeChain(site.inScope, localName, filePath, indexes)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Emit one IMPORTS edge per (caller, sourceFile) pair.
|
||||
if (!emittedImports.has(sourceFile)) {
|
||||
|
|
@ -277,4 +278,32 @@ const typescriptScopeResolver: ScopeResolver = {
|
|||
},
|
||||
};
|
||||
|
||||
|
||||
function hasLocalBindingInScopeChain(
|
||||
scopeId: ScopeId,
|
||||
name: string,
|
||||
filePath: string,
|
||||
indexes: ScopeResolutionIndexes,
|
||||
): boolean {
|
||||
const visited = new Set<ScopeId>();
|
||||
let cursor: ScopeId | null | undefined = scopeId;
|
||||
|
||||
while (cursor !== null && cursor !== undefined && !visited.has(cursor)) {
|
||||
visited.add(cursor);
|
||||
const scope = indexes.scopeTree.getScope(cursor);
|
||||
if (!scope) return false;
|
||||
|
||||
const localBindings = scope.bindings.get(name);
|
||||
if (
|
||||
localBindings?.some((binding) => binding.origin === 'local' && binding.def.filePath === filePath)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
cursor = scope.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export { typescriptScopeResolver };
|
||||
|
|
|
|||
3
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/.nuxt/imports.d.ts
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/.nuxt/imports.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { validate } from '../composables/clientValidate'
|
||||
export { useAuto, other } from '../composables/useAuto'
|
||||
export { useBarrel } from '../composables/group'
|
||||
5
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/app.ts
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/app.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export function renderClient() {
|
||||
validate();
|
||||
useAuto();
|
||||
serverOnly();
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/composables/clientValidate.ts
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/composables/clientValidate.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function validate() {
|
||||
return 'client';
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/composables/group/index.ts
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/composables/group/index.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function useBarrel() {
|
||||
return 'barrel';
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/composables/useAuto.ts
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/composables/useAuto.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export function useAuto() {
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
export function other() {
|
||||
return 'other';
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/pages/explicit-auto.ts
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/pages/explicit-auto.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { useAuto } from '../composables/useAuto';
|
||||
|
||||
export function explicitAuto() {
|
||||
useAuto();
|
||||
}
|
||||
6
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/pages/explicit.ts
vendored
Normal file
6
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/pages/explicit.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { other } from '../composables/useAuto';
|
||||
|
||||
export function explicitOther() {
|
||||
useAuto();
|
||||
other();
|
||||
}
|
||||
7
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/pages/local.ts
vendored
Normal file
7
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/pages/local.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export function useAuto() {
|
||||
return 'local';
|
||||
}
|
||||
|
||||
export function renderLocal() {
|
||||
useAuto();
|
||||
}
|
||||
8
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/pages/noise.ts
vendored
Normal file
8
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/pages/noise.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export class validate {}
|
||||
|
||||
const text = 'validate()';
|
||||
/* useAuto() */
|
||||
|
||||
export function noise() {
|
||||
new validate();
|
||||
}
|
||||
5
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/server/api/route.ts
vendored
Normal file
5
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/server/api/route.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export function handleServer() {
|
||||
validate();
|
||||
useAuto();
|
||||
useBarrel();
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/server/utils/serverOnly.ts
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/server/utils/serverOnly.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function serverOnly() {
|
||||
return 'server-only';
|
||||
}
|
||||
3
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/server/utils/serverValidate.ts
vendored
Normal file
3
gitnexus/test/fixtures/lang-resolution/nuxt-auto-imports/server/utils/serverValidate.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function validate() {
|
||||
return 'server';
|
||||
}
|
||||
106
gitnexus/test/integration/resolvers/nuxt-scope.test.ts
Normal file
106
gitnexus/test/integration/resolvers/nuxt-scope.test.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
FIXTURES,
|
||||
getRelationships,
|
||||
runPipelineFromRepo,
|
||||
type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
describe('Nuxt/Nitro auto-import scope resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'nuxt-auto-imports'), () => {}, {
|
||||
skipGraphPhases: true,
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
function nuxtCalls() {
|
||||
return getRelationships(result, 'CALLS').filter((edge) => edge.rel.reason === 'nuxt-auto-import');
|
||||
}
|
||||
|
||||
it('prefers server/utils over client composables for same-named server calls', () => {
|
||||
const calls = nuxtCalls();
|
||||
const serverValidate = calls.find(
|
||||
(edge) =>
|
||||
edge.sourceFilePath.endsWith('server/api/route.ts') &&
|
||||
edge.target === 'validate' &&
|
||||
edge.targetFilePath.endsWith('server/utils/serverValidate.ts'),
|
||||
);
|
||||
const wrongClientValidate = calls.find(
|
||||
(edge) =>
|
||||
edge.sourceFilePath.endsWith('server/api/route.ts') &&
|
||||
edge.target === 'validate' &&
|
||||
edge.targetFilePath.endsWith('composables/clientValidate.ts'),
|
||||
);
|
||||
|
||||
expect(serverValidate).toBeDefined();
|
||||
expect(wrongClientValidate).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps server/utils out of client files while allowing client auto-imports', () => {
|
||||
const calls = nuxtCalls();
|
||||
expect(
|
||||
calls.find(
|
||||
(edge) =>
|
||||
edge.sourceFilePath.endsWith('app.ts') &&
|
||||
edge.target === 'validate' &&
|
||||
edge.targetFilePath.endsWith('composables/clientValidate.ts'),
|
||||
),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
calls.find(
|
||||
(edge) =>
|
||||
edge.sourceFilePath.endsWith('app.ts') &&
|
||||
edge.targetFilePath.endsWith('server/utils/serverOnly.ts'),
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('resolves extensionless barrel directories to index files', () => {
|
||||
const calls = nuxtCalls();
|
||||
const imports = getRelationships(result, 'IMPORTS').filter(
|
||||
(edge) => edge.rel.reason === 'nuxt-auto-import-file',
|
||||
);
|
||||
|
||||
expect(
|
||||
calls.find(
|
||||
(edge) =>
|
||||
edge.sourceFilePath.endsWith('server/api/route.ts') &&
|
||||
edge.target === 'useBarrel' &&
|
||||
edge.targetFilePath.endsWith('composables/group/index.ts'),
|
||||
),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
imports.find(
|
||||
(edge) =>
|
||||
edge.sourceFilePath.endsWith('server/api/route.ts') &&
|
||||
edge.targetFilePath.endsWith('composables/group/index.ts'),
|
||||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('does not emit auto-import edges for local shadowing or lexical noise', () => {
|
||||
const calls = nuxtCalls();
|
||||
|
||||
expect(calls.filter((edge) => edge.sourceFilePath.endsWith('pages/local.ts'))).toHaveLength(0);
|
||||
expect(calls.filter((edge) => edge.sourceFilePath.endsWith('pages/noise.ts'))).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('suppresses only explicitly imported local names, not every symbol from the same source', () => {
|
||||
const calls = nuxtCalls();
|
||||
|
||||
expect(
|
||||
calls.find(
|
||||
(edge) => edge.sourceFilePath.endsWith('pages/explicit.ts') && edge.target === 'useAuto',
|
||||
),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
calls.find(
|
||||
(edge) =>
|
||||
edge.sourceFilePath.endsWith('pages/explicit-auto.ts') && edge.target === 'useAuto',
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
getNuxtAutoImportEntry,
|
||||
isNitroServerRuntimeFile,
|
||||
loadNuxtAutoImports,
|
||||
} from '../../../../src/core/ingestion/languages/typescript/nuxt-auto-imports.js';
|
||||
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
function makeRepo(): string {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-nuxt-auto-imports-'));
|
||||
tempRoots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
function writeFile(root: string, relPath: string, content: string): void {
|
||||
const fullPath = path.join(root, relPath);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, content, 'utf8');
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of tempRoots.splice(0)) {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('loadNuxtAutoImports', () => {
|
||||
it('returns null for non-Nuxt repos without .nuxt/imports.d.ts', async () => {
|
||||
const root = makeRepo();
|
||||
writeFile(root, 'server/utils/helpers.ts', 'export function serverOnly() {}');
|
||||
|
||||
await expect(loadNuxtAutoImports(root)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('keeps client and server entries separate and selects by caller scope', async () => {
|
||||
const root = makeRepo();
|
||||
writeFile(
|
||||
root,
|
||||
'.nuxt/imports.d.ts',
|
||||
"export { validate, useThing as useAlias } from '../composables/clientValidate'\n",
|
||||
);
|
||||
writeFile(root, 'composables/clientValidate.ts', 'export function validate() {}');
|
||||
writeFile(root, 'server/utils/serverValidate.ts', 'export function validate() {}');
|
||||
|
||||
const config = await loadNuxtAutoImports(root);
|
||||
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.clientByLocalName.get('validate')).toMatchObject({
|
||||
sourceFile: 'composables/clientValidate.ts',
|
||||
scope: 'client',
|
||||
});
|
||||
expect(config!.clientByLocalName.get('useAlias')).toMatchObject({
|
||||
exportName: 'useThing',
|
||||
sourceFile: 'composables/clientValidate.ts',
|
||||
scope: 'client',
|
||||
});
|
||||
expect(config!.serverByLocalName.get('validate')).toMatchObject({
|
||||
sourceFile: 'server/utils/serverValidate.ts',
|
||||
scope: 'server',
|
||||
});
|
||||
expect(getNuxtAutoImportEntry(config!, 'validate', 'server/api/users.ts')).toMatchObject({
|
||||
sourceFile: 'server/utils/serverValidate.ts',
|
||||
});
|
||||
expect(getNuxtAutoImportEntry(config!, 'validate', 'app.vue')).toMatchObject({
|
||||
sourceFile: 'composables/clientValidate.ts',
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves extensionless directory imports to index files', async () => {
|
||||
const root = makeRepo();
|
||||
writeFile(root, '.nuxt/imports.d.ts', "export { useGroup } from '../composables/group'\n");
|
||||
writeFile(root, 'composables/group/index.ts', 'export function useGroup() {}');
|
||||
|
||||
const config = await loadNuxtAutoImports(root);
|
||||
|
||||
expect(config!.clientByLocalName.get('useGroup')).toMatchObject({
|
||||
sourceFile: 'composables/group/index.ts',
|
||||
});
|
||||
});
|
||||
|
||||
it('indexes common server/utils export forms and skips generated dirs', async () => {
|
||||
const root = makeRepo();
|
||||
writeFile(root, '.nuxt/imports.d.ts', '');
|
||||
writeFile(
|
||||
root,
|
||||
'server/utils/helpers.ts',
|
||||
[
|
||||
'export function fn() {}',
|
||||
'export async function asyncFn() {}',
|
||||
'export class ServerThing {}',
|
||||
'export default function defaultTool() {}',
|
||||
'export const alpha = () => {}, beta = () => {};',
|
||||
'export let gamma = () => {};',
|
||||
'export var delta = () => {};',
|
||||
].join('\n'),
|
||||
);
|
||||
writeFile(root, 'server/utils/dist/generated.ts', 'export function generatedUtility() {}');
|
||||
|
||||
const config = await loadNuxtAutoImports(root);
|
||||
|
||||
expect([...config!.serverByLocalName.keys()].sort()).toEqual([
|
||||
'ServerThing',
|
||||
'alpha',
|
||||
'asyncFn',
|
||||
'beta',
|
||||
'defaultTool',
|
||||
'delta',
|
||||
'fn',
|
||||
'gamma',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNitroServerRuntimeFile', () => {
|
||||
it('matches only Nitro runtime entry directories', () => {
|
||||
expect(isNitroServerRuntimeFile('server/api/users.ts')).toBe(true);
|
||||
expect(isNitroServerRuntimeFile('server/routes/feed.ts')).toBe(true);
|
||||
expect(isNitroServerRuntimeFile('server/middleware/auth.ts')).toBe(true);
|
||||
expect(isNitroServerRuntimeFile('server/utils/auth.ts')).toBe(false);
|
||||
expect(isNitroServerRuntimeFile('pages/index.ts')).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue