fix(extraction): index Vue <template> component usages (#629 follow-up) (#659)

Vue's extractor parsed only the <script> block, so a component used solely
in another component's <template> (`<MyButton />`) produced no reference —
and thus showed a false 0 callers, even after the barrel-resolution fix in
PR #657. This is the Vue analogue of Svelte's extractTemplateComponents.

extractTemplateComponents() now scans the template (everything outside the
<script>/<style> blocks, which also handles nested <template> tags for
v-if/slots) for component tags:
- PascalCase tags (`<MyButton/>`) — captured as-is.
- kebab-case tags (`<my-button/>`) — converted to PascalCase so they match
  the imported component's name. Safe: an unmatched name creates no edge
  during resolution, so native custom elements just don't resolve.
- Native HTML elements (lowercase, no hyphen) and Vue built-ins
  (Transition, KeepAlive, …) are skipped.

Adds no nodes — only `references` — so node counts stay stable. With this
plus #657, a Vue component re-exported through a barrel and used only in a
template now resolves end-to-end (callers/impact/callees).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-02 21:44:27 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent bdfd55e69c
commit 629d8472b1
4 changed files with 150 additions and 0 deletions
+31
View File
@@ -1389,6 +1389,37 @@ func main() {
const callers = cg.getCallers(runNode!.id);
expect(callers.some((c) => c.node.filePath === 'src/App.vue')).toBe(true);
});
it('follows a Vue component used in a <template> through a default re-export barrel (#629)', async () => {
// End-to-end Vue analogue of the Svelte case: the leaf is a `.vue`
// component re-exported under an alias (`Thing`) that differs from its
// real name (`Widget`), and the consumer uses it ONLY in markup
// (`<Thing />`). Requires both the new template-tag extraction AND the
// barrel default-export chase to connect the edge.
fs.mkdirSync(path.join(tempDir, 'src/lib'), { recursive: true });
fs.writeFileSync(
path.join(tempDir, 'src/lib/Widget.vue'),
`<script setup lang="ts">\ndefineProps<{ label?: string }>();\n</script>\n<template><button>x</button></template>\n`
);
fs.writeFileSync(
path.join(tempDir, 'src/lib/index.ts'),
`export { default as Thing } from './Widget.vue';\n`
);
fs.writeFileSync(
path.join(tempDir, 'src/App.vue'),
`<script setup lang="ts">\nimport { Thing } from './lib';\n</script>\n<template>\n <Thing />\n</template>\n`
);
cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();
const widgetNode = cg
.getNodesByKind('component')
.find((n) => n.name === 'Widget' && n.filePath === 'src/lib/Widget.vue');
expect(widgetNode).toBeDefined();
const callers = cg.getCallers(widgetNode!.id);
expect(callers.some((c) => c.node.filePath === 'src/App.vue')).toBe(true);
});
});
describe('C/C++ Import Resolution', () => {