From 2fc4069430a90b8451767d8da357d96c165f4017 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Wed, 24 Jun 2026 08:37:46 +0000 Subject: [PATCH] fix(ingestion): capture only LHS binding names in Nitro server-util exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Nuxt server-util export scanner ran a declarator regex over the whole `export const …` right-hand side, so it registered RHS tokens as auto-import names: arrow-function parameters (`export const f = (event) => …` → `event`), object-literal keys (`export const c = { onError } ` → `onError`), and bare operands. It also dropped generic-typed declarators (`export const x: Map = …`) because the type-annotation skip broke at the comma inside the generic. Both produced wrong/missing auto-import CALLS edges. Capture only the leading binding name of each top-level declarator via a depth-aware comma splitter (tracks (), [], {}, <>), skipping destructuring patterns. Nitro auto-imports only surface top-level binding names, so the RHS is never parsed. Adds unit coverage for the param/object-key/operand/generic and multi-declarator forms. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01T4W25WLfYD1JNy8icxeLPU --- .../languages/typescript/nuxt-auto-imports.ts | 42 ++++++++++++++++--- .../typescript/nuxt-auto-imports.test.ts | 30 +++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/gitnexus/src/core/ingestion/languages/typescript/nuxt-auto-imports.ts b/gitnexus/src/core/ingestion/languages/typescript/nuxt-auto-imports.ts index 608082136..205d66383 100644 --- a/gitnexus/src/core/ingestion/languages/typescript/nuxt-auto-imports.ts +++ b/gitnexus/src/core/ingestion/languages/typescript/nuxt-auto-imports.ts @@ -77,7 +77,9 @@ const IMPORTS_DTS_EXPORT_RE = /^export\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g 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; +// Matches the binding name at the head of a single declarator (`name`, `name:`, `name =`). +// A leading `{`/`[` (destructuring) does not match, so destructured exports are skipped. +const DECLARATOR_NAME_RE = /^\s*([A-Za-z_$][A-Za-z0-9_$]*)/; // ---- loader ----------------------------------------------------------------- @@ -309,13 +311,41 @@ function extractNitroExportNames(content: string): string[] { 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]!); + // Only the LHS binding name of each top-level declarator is a Nitro export. + // Splitting on top-level commas and reading the leading identifier avoids + // capturing RHS tokens (arrow params, object keys, operands) as export names, + // and tolerates commas inside generic type annotations (`x: Map = …`). + for (const declarator of splitTopLevelDeclarators(variableDeclaration[1]!)) { + const name = DECLARATOR_NAME_RE.exec(declarator); + if (name) names.add(name[1]!); } } return [...names]; } + +/** + * Split a `const`/`let`/`var` declarator list on top-level commas, tracking + * `()`, `[]`, `{}`, and `<>` nesting so commas inside call args, object/array + * literals, and generic type arguments do not split a declarator. Errs toward + * under-splitting on pathological RHS (a missed binding name, never a spurious + * one) — Nitro only auto-imports real top-level binding names. + */ +function splitTopLevelDeclarators(text: string): string[] { + const parts: string[] = []; + let depth = 0; + let start = 0; + for (let i = 0; i < text.length; i++) { + const ch = text[i]; + if (ch === '(' || ch === '[' || ch === '{' || ch === '<') { + depth++; + } else if (ch === ')' || ch === ']' || ch === '}' || ch === '>') { + if (depth > 0) depth--; + } else if (ch === ',' && depth === 0) { + parts.push(text.slice(start, i)); + start = i + 1; + } + } + parts.push(text.slice(start)); + return parts; +} diff --git a/gitnexus/test/unit/scope-resolution/typescript/nuxt-auto-imports.test.ts b/gitnexus/test/unit/scope-resolution/typescript/nuxt-auto-imports.test.ts index 31149c5ad..19ac329be 100644 --- a/gitnexus/test/unit/scope-resolution/typescript/nuxt-auto-imports.test.ts +++ b/gitnexus/test/unit/scope-resolution/typescript/nuxt-auto-imports.test.ts @@ -118,6 +118,36 @@ describe('loadNuxtAutoImports', () => { sourceFile: 'server/utils/jsHelper.js', }); }); + + it('captures only LHS binding names from server/utils const exports', async () => { + const root = makeRepo(); + writeFile(root, '.nuxt/imports.d.ts', ''); + writeFile( + root, + 'server/utils/forms.ts', + [ + 'export const createUser = async (event: H3Event) => {};', + 'export const config = { onError: () => {} };', + 'export const eq = a === b;', + 'export const helper: Record = {};', + 'export const first = 1, second = 2;', + ].join('\n'), + ); + + const config = await loadNuxtAutoImports(root); + + // Only declared binding names — never RHS arrow params (`event`), object + // keys (`onError`), operands (`a`/`b`), nor a generic-typed name dropped at + // the comma inside `Record`. + expect([...config!.serverByLocalName.keys()].sort()).toEqual([ + 'config', + 'createUser', + 'eq', + 'first', + 'helper', + 'second', + ]); + }); }); describe('isNitroServerRuntimeFile', () => {