fix(ingestion): stop Nitro server callers resolving client composables

`getNuxtAutoImportEntry` fell back to the client composable map when a
`server/api|routes|middleware` caller's name had no `server/utils` entry. But
Nitro only auto-imports `server/utils/**` into the server context — app
`composables/` are Vue-app-only — so that fallback minted CALLS/IMPORTS edges
Nitro never creates (e.g. a server route "calling" a composable it cannot see
without an explicit import).

Server callers now resolve the server map only. Restructure the barrel-directory
integration test to use a client caller (which legitimately auto-imports the
composable) so `index.*` resolution stays covered, and add a negative assertion
that `server/api/route.ts` emits no edge to `composables/*` while its real
`server/utils` call still resolves. Unit test locks that a server caller does
not fall back to a client-only name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4W25WLfYD1JNy8icxeLPU
This commit is contained in:
Gergo Magyar 2026-06-24 08:40:09 +00:00
parent 2fc4069430
commit 6fbe4abb73
4 changed files with 33 additions and 3 deletions

View file

@ -123,7 +123,11 @@ export function getNuxtAutoImportEntry(
callerFile: string,
): NuxtAutoImportEntry | undefined {
if (isNitroServerRuntimeFile(callerFile)) {
return config.serverByLocalName.get(localName) ?? config.clientByLocalName.get(localName);
// Nitro auto-imports only `server/utils/**` into the server context; app
// `composables/` are Vue-app-only. Server callers therefore resolve the
// server map only — no client fallback, which would emit cross-context
// CALLS edges Nitro never actually creates.
return config.serverByLocalName.get(localName);
}
return config.clientByLocalName.get(localName);
}

View file

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

View file

@ -64,7 +64,7 @@ describe('Nuxt/Nitro auto-import scope resolution', () => {
expect(
calls.find(
(edge) =>
edge.sourceFilePath.endsWith('server/api/route.ts') &&
edge.sourceFilePath.endsWith('app.ts') &&
edge.target === 'useBarrel' &&
edge.targetFilePath.endsWith('composables/group/index.ts'),
),
@ -72,12 +72,35 @@ describe('Nuxt/Nitro auto-import scope resolution', () => {
expect(
imports.find(
(edge) =>
edge.sourceFilePath.endsWith('server/api/route.ts') &&
edge.sourceFilePath.endsWith('app.ts') &&
edge.targetFilePath.endsWith('composables/group/index.ts'),
),
).toBeDefined();
});
it('does not resolve client composables from Nitro server callers (no client fallback)', () => {
const calls = nuxtCalls();
// server/api/route.ts calls validate() (a real server/util), useAuto() and
// useBarrel() (client-only composables). Only the server/util resolves;
// Nitro does not auto-import composables/ server-side, so no edge is emitted
// to either composable.
const composableEdges = calls.filter(
(edge) =>
edge.sourceFilePath.endsWith('server/api/route.ts') &&
edge.targetFilePath.includes('/composables/'),
);
expect(composableEdges).toHaveLength(0);
// The legitimate server/util edge still resolves.
expect(
calls.find(
(edge) =>
edge.sourceFilePath.endsWith('server/api/route.ts') &&
edge.target === 'validate' &&
edge.targetFilePath.endsWith('server/utils/serverValidate.ts'),
),
).toBeDefined();
});
it('does not emit auto-import edges for local shadowing or lexical noise', () => {
const calls = nuxtCalls();

View file

@ -68,6 +68,8 @@ describe('loadNuxtAutoImports', () => {
expect(getNuxtAutoImportEntry(config!, 'validate', 'app.vue')).toMatchObject({
sourceFile: 'composables/clientValidate.ts',
});
// A Nitro server caller does NOT fall back to a client-only composable.
expect(getNuxtAutoImportEntry(config!, 'useAlias', 'server/api/users.ts')).toBeUndefined();
});
it('resolves extensionless directory imports to index files', async () => {