feat: ✨ resolve Nuxt/Nitro auto-imports in TypeScript scope resolver#2026
feat: ✨ resolve Nuxt/Nitro auto-imports in TypeScript scope resolver#2026slugb0t wants to merge 6 commits into
Conversation
|
@slugb0t is attempting to deploy a commit to the NexusCore Team on Vercel. A member of the Team first needs to authorize it. |
magyargergo
left a comment
There was a problem hiding this comment.
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.
emitPostResolutionEdgesis a real, orchestrator-invoked contract hook (run.ts:590) already used by the Vue resolver;ctx.fileContents/resolutionConfigare threaded;nuxtAutoImportsrides the same proven channel astsconfigPaths. Codex confirmedgenerateId('File', filePath)matches the canonical File-node id (structure-processor.ts) — no dangling-node risk. - Zero-cost for non-Nuxt repos (perf + reproduced):
loadNuxtAutoImportsreturns null on one failedfs.readFilewhen.nuxt/imports.d.tsis 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/#appsources 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/utilssymlink-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
nuxt-auto-imports.ts:141— P1, client/server name collision → WRONG-target edge[reproduced].byLocalNameis a single first-wins map, but Nuxt composables (client/shared scope) andserver/utils(server scope) are disjoint. When both export the same name (e.g.validate), the composable wins (reproduced: the map keepscomposables/clientValidate.ts) and aserver/apifile callingvalidate()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.scope-resolver.ts:236— P1, local-definition shadowing emits a spurious cross-file edge[reproduced]. The only suppression isexplicitImports.has(sourceFile) || sourceFile === filePath; a file that defines its ownfunction useFoo(){}(or a param/local nameduseFoo) and calls it gets a bogusnuxt-auto-importCALLS+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 whenlocalNameresolves to a same-file/local binding.scope-resolver.ts:207-208— P2,CALL_RElexical false positives; the comment is factually wrong[reproduced]. The comment says it "Excludesnew X(", but the lookbehind(?<![.\w])does not —new Foo(matches (reproduced →["Foo"]). It also matches inside block comments, strings, template literals, and JSX text, and matches the declaration sitefunction useFoo(. ConverselyuseFoo?.()/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.nuxt-auto-imports.ts:200— P2,resolveExtensionmatches a directory, dropping barrel composables[reproduced]. The trailing''candidate +fs.accesssucceeds on a directory, soexport { useG } from '../composables/group'(withgroup/index.ts) resolves tosourceFile: 'composables/group'(the dir, reproduced). The File-id and node lookups then miss, so both edges silently fail — no/index.tsfallback (a silent skip, hence P2). Add anisFile()check + explicitindex.*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/#appfiltering, non-Nuxt→null, the server/utils gate) or the end-to-end edge emission. The analogous VueemitPostResolutionEdgesis covered by a fixture + edge-assertion suite — the Nuxt path should mirror it (atest/unit/nuxt-auto-imports.test.tsloader suite + atest/integration/resolvers/nuxt-scope.test.tsasserting CALLS/IMPORTS edges withreason:'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 checksshows 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 VueemitPostResolutionEdgesprecedent, so it's a known tradeoff rather than a regression — worth a comment, not a blocker. server/utilsexportRecoverage gaps (Codex + correctness + maintainability, P3): missesexport class,export let/var,export default function, and the 2nd declarator inexport const a=1, b=2. Nitro auto-imports all of these — document or widen.collectTsFilesskips no generated dirs (maintainability, P3): unlike the C/C++ walkers it scansdist/build/.nextunderserver/. Consider a sharedwalkFilesByExtensionskip-set.- Perf nits (P3):
resolveExtensiondoes up to 5×N serialfs.access(parallelize);exportReallocated 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 XCALLS 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)) { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
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', '']) { |
There was a problem hiding this comment.
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.
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
importstatements: client composables via.nuxt/imports.d.tsand server utilities via theserver/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
.nuxt/imports.d.tsto extract project-local composable auto-imports (node_modules and#app/...runtime paths are skipped)server/utils/**recursively for Nitro server-util auto-importsCALLSandIMPORTSedges viaemitPostResolutionEdgesafter all standard resolution passesnullwhen.nuxt/is absent)Explicitly out of scope / not done here
confidence: 0.75).nuxt/components.d.ts) -- component edges are already handled by the Vue resolverImplementation notes
Two files changed:
nuxt-auto-imports.ts(new) -- loads the auto-import map at workspace analysis time. Reads.nuxt/imports.d.tsfor app-layer composables and recursively scansserver/utils/**for Nitro server util exports. Both sources produce the sameNuxtAutoImportEntryshape so the edge-emission pass treats them uniformly.scope-resolver.ts(modified) -- extendsTypescriptResolutionConfigwithnuxtAutoImports, loads the map inloadResolutionConfig, and addsemitPostResolutionEdges. 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 oneIMPORTSedge per (caller, source file) pair and oneCALLSedge 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:
The +390 edges and +10 execution flows represent previously invisible call chains. Confirmed that
server/api/release/zenodo/index.post.tsnow shows an incoming CALLS edge tobeginZenodoPublicationinserver/utils/zenodo.ts, which was the original disconnected node that motivated this change.cd gitnexus && npm testcd gitnexus && npm run test:integration(if core/indexing/MCP paths changed)cd gitnexus && npx tsc --noEmitRisk & rollout
.nuxt/is absent).npx gitnexus analyze. Edge count increase is expected and reflects recovered call graph connectivity.confidence: 0.75to distinguish them from fully resolved edges.Checklist
AGENTS.md/ overlays changed: headers, scope block, and changelog updated per project conventionsSummary 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:
.nuxt/imports.d.tsand scansserver/utils/**for project-local auto-imported symbols.Enhancements:
.nuxt/is absent.