Skip to content

feat: ✨ resolve Nuxt/Nitro auto-imports in TypeScript scope resolver#2026

Open
slugb0t wants to merge 6 commits into
abhigyanpatwari:mainfrom
slugb0t:feat/nuxt-auto-imports
Open

feat: ✨ resolve Nuxt/Nitro auto-imports in TypeScript scope resolver#2026
slugb0t wants to merge 6 commits into
abhigyanpatwari:mainfrom
slugb0t:feat/nuxt-auto-imports

Conversation

@slugb0t

@slugb0t slugb0t commented Jun 4, 2026

Copy link
Copy Markdown

Summary

Adds Nuxt v4 and Nitro auto-import awareness to the TypeScript scope resolver so that composables and server utils used without explicit import statements produce CALLS and IMPORTS edges in the knowledge graph.

Motivation / context

Nuxt and Nitro make symbols available project-wide without import statements: client composables via .nuxt/imports.d.ts and server utilities via the server/utils/** convention. Because the standard scope-resolution passes only follow explicit imports, every call to an auto-imported symbol produces no graph edges. API route handlers, server utils, and shared composables end up disconnected even when actively called.

Areas touched

  • gitnexus/ (CLI / core / MCP server)

Scope & constraints

In scope

  • Parsing .nuxt/imports.d.ts to extract project-local composable auto-imports (node_modules and #app/... runtime paths are skipped)
  • Scanning server/utils/** recursively for Nitro server-util auto-imports
  • Emitting CALLS and IMPORTS edges via emitPostResolutionEdges after all standard resolution passes
  • Skipping self-referential edges (a file cannot auto-import its own exports)
  • Skipping symbols already covered by an explicit import
  • Zero overhead for non-Nuxt TypeScript projects (returns null when .nuxt/ is absent)

Explicitly out of scope / not done here

  • Filtering string literals and comments from the content scanner (heuristic limitation; edges are tagged confidence: 0.75)
  • Following barrel re-exports that chain through multiple files
  • Nuxt component auto-imports (.nuxt/components.d.ts) -- component edges are already handled by the Vue resolver

Implementation notes

Two files changed:

nuxt-auto-imports.ts (new) -- loads the auto-import map at workspace analysis time. Reads .nuxt/imports.d.ts for app-layer composables and recursively scans server/utils/** for Nitro server util exports. Both sources produce the same NuxtAutoImportEntry shape so the edge-emission pass treats them uniformly.

scope-resolver.ts (modified) -- extends TypescriptResolutionConfig with nuxtAutoImports, loads the map in loadResolutionConfig, and adds emitPostResolutionEdges. The hook scans raw file content with /(?<![.\w])([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g, checks each identifier against the auto-import map, and emits one IMPORTS edge per (caller, source file) pair and one CALLS edge per (caller, symbol) pair.

Edge provenance: reason: 'nuxt-auto-import', confidence: 0.75 (below the 0.9 used for fully resolved edges to signal heuristic origin).

Testing & verification

Tested against a real Nuxt v4 project with 148 indexed files:

Metric Before After
Nodes 1,803 1,810
Edges 2,089 2,479
Clusters 43 50
Execution flows 14 24

The +390 edges and +10 execution flows represent previously invisible call chains. Confirmed that server/api/release/zenodo/index.post.ts now shows an incoming CALLS edge to beginZenodoPublication in server/utils/zenodo.ts, which was the original disconnected node that motivated this change.

  • cd gitnexus && npm test
  • cd gitnexus && npm run test:integration (if core/indexing/MCP paths changed)
  • cd gitnexus && npx tsc --noEmit

Risk & rollout

  • No breaking changes. Non-Nuxt projects are unaffected (early return when .nuxt/ is absent).
  • Nuxt projects will see additional edges on next npx gitnexus analyze. Edge count increase is expected and reflects recovered call graph connectivity.
  • False-positive edges are possible when auto-imported symbol names appear in string literals or comments. These are tagged confidence: 0.75 to distinguish them from fully resolved edges.

Checklist

  • PR body meets repo minimum length (workflow may label short descriptions)
  • If AGENTS.md / overlays changed: headers, scope block, and changelog updated per project conventions
  • No secrets, tokens, or machine-specific paths committed

Summary by Sourcery

Add Nuxt/Nitro auto-import awareness to the TypeScript scope resolver to recover call and import edges for auto-imported symbols in Nuxt projects.

New Features:

  • Introduce Nuxt auto-import configuration loading that parses .nuxt/imports.d.ts and scans server/utils/** for project-local auto-imported symbols.
  • Emit heuristic CALLS and IMPORTS edges for Nuxt/Nitro auto-imported functions used without explicit imports in TypeScript files.

Enhancements:

  • Ensure Nuxt-specific auto-import handling is a no-op for non-Nuxt repositories by returning null when .nuxt/ is absent.

@vercel

vercel Bot commented Jun 4, 2026

Copy link
Copy Markdown

@slugb0t is attempting to deploy a commit to the NexusCore Team on Vercel.

A member of the Team first needs to authorize it.

@slugb0t slugb0t requested a review from magyargergo as a code owner June 4, 2026 17:35

@magyargergo magyargergo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tri-review digest — PR #2026 feat: resolve Nuxt/Nitro auto-imports in TypeScript scope resolver

Methods (engine breakdown): 5 Claude persona lanes returned structured findings — Compound-Engineering correctness, adversarial, performance, maintainability, testing — plus Codex (the one independent engine), live with real findings. (The two GitNexus lanes — risk, test/CI — ended mid-investigation; their domains are covered below.) Engine breakdown: 5 Claude lanes + 1 Codex. Independence is asymmetric (the Claude lanes are correlated); the strong signals below are where Codex and a Claude lane converge, several of which the coordinator also reproduced.

Verdict: not merge-ready as-is. The architecture is sound and well-wired, but the heuristic has reproduced correctness bugs — including a P1 wrong-target edge (a server route's validate() resolves to the wrong file) and a P1 false-positive edge from local shadowing — and it ships with ZERO tests while CI has not run (only Vercel reported). The blast radius is contained (the whole feature is gated off for non-Nuxt repos, and edges are confidence-tagged 0.75, so there is no crash/regression risk to ordinary TypeScript analysis), but the inline items are real correctness defects and nothing is verified by tests — they should be fixed and covered before merge. (Posted as a collaborative COMMENT, not REQUEST_CHANGES.)

Credit — what's solid (validated)

  • Wiring is correct. emitPostResolutionEdges is a real, orchestrator-invoked contract hook (run.ts:590) already used by the Vue resolver; ctx.fileContents/resolutionConfig are threaded; nuxtAutoImports rides the same proven channel as tsconfigPaths. Codex confirmed generateId('File', filePath) matches the canonical File-node id (structure-processor.ts) — no dangling-node risk.
  • Zero-cost for non-Nuxt repos (perf + reproduced): loadNuxtAutoImports returns null on one failed fs.readFile when .nuxt/imports.d.ts is absent; the hook early-returns. The whole content scan is behind that gate. The non-Nuxt no-op is verified.
  • Parser works on a realistic fixture (multi-symbol, default as X, multi-line blocks; node_modules/#app sources correctly skipped). The perf lane found the hook is algorithmically better than the Vue precedent (pre-builds the explicit-imports index once vs Vue's per-file rebuild).
  • Refuted hazards: ReDoS / catastrophic backtracking (the [^}]+/[^'"]+ regexes are linear — tested to 4 MB), server/utils symlink-loop recursion (Dirent doesn't follow symlinks), and ../-traversal sources escaping the repo (produce inert edges). All probed and cleared.

Inline findings (4, all reproduced) — two P1, two P2

  1. nuxt-auto-imports.ts:141 — P1, client/server name collision → WRONG-target edge [reproduced]. byLocalName is a single first-wins map, but Nuxt composables (client/shared scope) and server/utils (server scope) are disjoint. When both export the same name (e.g. validate), the composable wins (reproduced: the map keeps composables/clientValidate.ts) and a server/api file calling validate() gets a CALLS edge to the wrong file — the hook applies the flat map uniformly with no server/client gate. This is a correctness inversion (wrong node, not a missing/heuristic edge), hence P1. Fix: track client vs server scopes separately and select by caller location.
  2. scope-resolver.ts:236 — P1, local-definition shadowing emits a spurious cross-file edge [reproduced]. The only suppression is explicitImports.has(sourceFile) || sourceFile === filePath; a file that defines its own function useFoo(){} (or a param/local named useFoo) and calls it gets a bogus nuxt-auto-import CALLS+IMPORTS edge to the unrelated composable (coexisting with the correct local edge, since the graph dedups by id). Codex independently rated this P1; adversarial + correctness also reached it — the headline cross-engine finding. Fix: skip emission when localName resolves to a same-file/local binding.
  3. scope-resolver.ts:207-208 — P2, CALL_RE lexical false positives; the comment is factually wrong [reproduced]. The comment says it "Excludes new X(", but the lookbehind (?<![.\w]) does notnew Foo( matches (reproduced → ["Foo"]). It also matches inside block comments, strings, template literals, and JSX text, and matches the declaration site function useFoo(. Conversely useFoo?.() / useFoo!() are silently missed. (Codex + adversarial + maintainability + reproduced.) The structural fix is to iterate the already-parsed call nodes rather than regex raw content. P2 because edges are 0.75-confidence-tagged.
  4. nuxt-auto-imports.ts:200 — P2, resolveExtension matches a directory, dropping barrel composables [reproduced]. The trailing '' candidate + fs.access succeeds on a directory, so export { useG } from '../composables/group' (with group/index.ts) resolves to sourceFile: 'composables/group' (the dir, reproduced). The File-id and node lookups then miss, so both edges silently fail — no /index.ts fallback (a silent skip, hence P2). Add an isFile() check + explicit index.* probing.

The big gap — zero tests + unreported CI

  • The 375-line feature adds NO test files (testing lane, P0-class). Nothing exercises loadNuxtAutoImports (parsing, default as, node_modules/#app filtering, non-Nuxt→null, the server/utils gate) or the end-to-end edge emission. The analogous Vue emitPostResolutionEdges is covered by a fixture + edge-assertion suite — the Nuxt path should mirror it (a test/unit/nuxt-auto-imports.test.ts loader suite + a test/integration/resolvers/nuxt-scope.test.ts asserting CALLS/IMPORTS edges with reason:'nuxt-auto-import', confidence 0.75, the explicit-import suppression, the self-edge guard, and the non-Nuxt no-op).
  • CI has not validated this commit: gh pr checks shows only Vercel (auth-fail) on the head — typecheck/lint/vitest have not reported (likely awaiting fork-PR workflow approval). So tests/typecheck are UNKNOWN, not green. The loader compiles/runs under tsx (coordinator-reproduced), but the full suite/typecheck is unconfirmed.

Lower-priority (body)

  • File→function CALLS shape (Codex, P2-design): auto-import CALLS always use sourceId: fileId, whereas fully-resolved TS CALLS use a callable/method anchor (resolveCallerGraphId). This is inherent to a content-scan heuristic (no enclosing-callable is resolved) and matches the existing Vue emitPostResolutionEdges precedent, so it's a known tradeoff rather than a regression — worth a comment, not a blocker.
  • server/utils exportRe coverage gaps (Codex + correctness + maintainability, P3): misses export class, export let/var, export default function, and the 2nd declarator in export const a=1, b=2. Nitro auto-imports all of these — document or widen.
  • collectTsFiles skips no generated dirs (maintainability, P3): unlike the C/C++ walkers it scans dist/build/.next under server/. Consider a shared walkFilesByExtension skip-set.
  • Perf nits (P3): resolveExtension does up to 5×N serial fs.access (parallelize); exportRe allocated per file (hoist). Both bounded; one-time per pass.
  • Per-source (not per-symbol) explicit-import suppression (correctness, P3): importing symbol A from a source suppresses an auto-import of symbol B from the same source (false negative; low practical impact).
  • default as X CALLS resolution is best-effort (IMPORTS emits; CALLS may not resolve if the default export is differently named) — documented; acceptable.

Coverage

Full diff reviewed (2 files, 375 lines, read in full + loader behavior reproduced). The gitnexus-* risk and test/CI lanes ended mid-investigation; their domains are covered by the CE correctness/adversarial/performance and testing lanes respectively.

Automated multi-tool digest (5 Claude persona lanes + live Codex; 4 inline findings reproduced by the coordinator). Verify before acting.


if (!localName || !exportName) continue;

if (!byLocalName.has(localName)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 · correctness · [reproduced] — wrong-target edge from client/server name collision.

byLocalName is a single first-wins map (if (!byLocalName.has(localName))), but Nuxt composables (client/shared scope) and server/utils (server scope) are disjoint auto-import scopes. When both export the same name they collapse here. Reproduced: with composables/clientValidate.ts exporting validate and server/utils/serverValidate.ts also exporting validate, the map keeps only {localName:'validate', sourceFile:'composables/clientValidate.ts'}. Because emitPostResolutionEdges applies the flat map uniformly with no caller-location gate, a server/api/*.ts route calling validate() then gets a CALLS edge to the wrong file (the client composable), not the server util it actually resolves to. That's a wrong-target inversion, not a missing/heuristic edge.

Fix: track client (composables/imports.d.ts) and server (server/utils) entries in separate maps and select by whether the caller file is under server/ (api/routes/middleware); at minimum, don't let a composable shadow a same-named server util.


// 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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 · correctness · [reproduced] — local definitions are not suppressed, so a file that defines its own symbol gets a spurious cross-file edge. (Codex independently rated this P1; adversarial + correctness also reached it — the strongest cross-engine finding here.)

The only suppression is explicitImports.has(sourceFile) || sourceFile === filePath. A file that defines its own function useFoo(){} (or has a param / local named useFoo) and calls useFoo() produces no import edge, so the guard misses it and a bogus nuxt-auto-import CALLS+IMPORTS edge to the unrelated composable is emitted — it coexists with the correct local edge (the graph dedups by id, and this edge has a distinct id/reason). Reproduced: CALL_RE matches both the declaration function useFoo( and the call useFoo().

Fix: before emitting, skip when localName resolves to a same-file/local binding (consult the file's own defined/imported symbols), mirroring the handledSites discipline the other post-resolution hooks use.

}

// Regex matches bare identifier call sites: word-boundary + name + "(".
// Excludes `new X(` (constructor calls are not free-function auto-imports).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 · correctness · [reproduced]CALL_RE has no lexical awareness, and this comment is factually wrong.

The comment claims it "Excludes new X(", but the lookbehind (?<![.\w]) only blocks a preceding ./word-char — in new Foo( the char before Foo is a space, so it matches (reproduced → ["Foo"]). The same raw-content scan also matches inside block comments (/* useFoo() */["useFoo"]), string/template literals, and JSX text, and matches the declaration site function useFoo(. Conversely it silently misses useFoo?.() and useFoo!(). The module doc only acknowledges string + single-line-comment false positives. Edges are 0.75-tagged so this is P2, but it's a steady source of bogus edges.

Fix: either correct the comment and broaden the documented limitations, or (better) iterate the already-parsed tree-sitter call nodes in parsedFile instead of regexing raw source — that removes the comment/string/new/JSX false-positive class entirely.

}

async function resolveExtension(base: string): Promise<string | null> {
for (const ext of ['.ts', '.tsx', '.js', '.jsx', '']) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 · correctness · [reproduced]resolveExtension matches a directory, silently dropping barrel composables.

The candidate list ends with '', and fs.access succeeds on a directory. So a barrel source export { useG } from '../composables/group' (where group/index.ts holds the export) resolves to sourceFile: 'composables/group' — the directory — reproduced. Downstream, generateId('File','composables/group') and simpleKey('composables/group','useG') match no node, so both the IMPORTS and CALLS edges silently fail (the index.ts is never reached). A silent skip, hence P2.

Fix: require the resolved candidate to be a regular file ((await fs.stat(c)).isFile()) and add explicit index.ts/.tsx/.js/.jsx probes for directory sources.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants