fix: scope Nuxt auto-import resolution

This commit is contained in:
Gergo Magyar 2026-06-24 06:54:18 +00:00
parent 1bc4cd4586
commit 83b1246e45
16 changed files with 466 additions and 73 deletions

View file

@ -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];
}

View file

@ -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 };

View file

@ -0,0 +1,3 @@
export { validate } from '../composables/clientValidate'
export { useAuto, other } from '../composables/useAuto'
export { useBarrel } from '../composables/group'

View file

@ -0,0 +1,5 @@
export function renderClient() {
validate();
useAuto();
serverOnly();
}

View file

@ -0,0 +1,3 @@
export function validate() {
return 'client';
}

View file

@ -0,0 +1,3 @@
export function useBarrel() {
return 'barrel';
}

View file

@ -0,0 +1,7 @@
export function useAuto() {
return 'auto';
}
export function other() {
return 'other';
}

View file

@ -0,0 +1,5 @@
import { useAuto } from '../composables/useAuto';
export function explicitAuto() {
useAuto();
}

View file

@ -0,0 +1,6 @@
import { other } from '../composables/useAuto';
export function explicitOther() {
useAuto();
other();
}

View file

@ -0,0 +1,7 @@
export function useAuto() {
return 'local';
}
export function renderLocal() {
useAuto();
}

View file

@ -0,0 +1,8 @@
export class validate {}
const text = 'validate()';
/* useAuto() */
export function noise() {
new validate();
}

View file

@ -0,0 +1,5 @@
export function handleServer() {
validate();
useAuto();
useBarrel();
}

View file

@ -0,0 +1,3 @@
export function serverOnly() {
return 'server-only';
}

View file

@ -0,0 +1,3 @@
export function validate() {
return 'server';
}

View 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();
});
});

View file

@ -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);
});
});