feat(extraction+resolution): Astro support — frontmatter/template extraction + src/pages routes (#768) (#815)

.astro files were not indexed at all, leaving a typical Astro site mostly
invisible to search/impact/explore. New AstroExtractor (Svelte/Vue SFC
pattern): component node per file, TS frontmatter + <script> blocks
delegated to the TypeScript extractor, template {fn(...)} calls (incl. the
multiline `{posts.map((post) => (` opening line), PascalCase component-tag
references. New astroResolver: Astro global + astro:* virtual modules as
framework-provided, component resolution with the #764 ambiguity rule,
src/pages/ file-based routes ([param]→:param, [...rest]→*rest, _-prefixed
and *.config.* excluded). SFC languages now preload the TS/JS grammars
their extractors delegate to (a pure-SFC file set previously had none
loaded). Also fixes a pre-existing Svelte/Vue script-block off-by-one that
reported every script symbol one line low.

Validated per the playbook: stalux (the issue's repro) 54/54 .astro files
indexed, getIconNode found at its exact line, 14/14 routes, 93.0% fair
cross-file coverage; AstroPaper 27/27 components, 13/13 routes (underscore
dirs correctly excluded), explore connects page→Card→Datetime through the
jsx-render synthesizer; node/edge counts stable across re-syncs.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-11 18:50:11 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 763ee9c825
commit 823ffd1c3d
15 changed files with 934 additions and 16 deletions
+41
View File
@@ -1438,6 +1438,47 @@ func main() {
expect(callers.some((c) => c.node.filePath === 'src/Bar.svelte')).toBe(true);
});
it('links an .astro page to the component and TS util it uses (#768)', async () => {
// The canonical Astro shape: a page imports a layout/component in
// frontmatter and uses it as a template tag; the component's template
// calls an imported .ts util. Both hops must produce graph edges or
// an Astro project is invisible to callers/impact.
fs.mkdirSync(path.join(tempDir, 'src/components'), { recursive: true });
fs.mkdirSync(path.join(tempDir, 'src/utils'), { recursive: true });
fs.mkdirSync(path.join(tempDir, 'src/pages'), { recursive: true });
fs.writeFileSync(
path.join(tempDir, 'src/utils/format.ts'),
`export function formatDate(d: Date): string { return d.toISOString(); }\n`
);
fs.writeFileSync(
path.join(tempDir, 'src/components/PostCard.astro'),
`---\nimport { formatDate } from '../utils/format';\nconst { date } = Astro.props;\n---\n<time>{formatDate(date)}</time>\n`
);
fs.writeFileSync(
path.join(tempDir, 'src/pages/index.astro'),
`---\nimport PostCard from '../components/PostCard.astro';\n---\n<PostCard date={new Date()} />\n`
);
cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();
// Hop 1: page → component (template tag through the frontmatter import)
const cardNode = cg
.getNodesByKind('component')
.find((n) => n.name === 'PostCard' && n.filePath === 'src/components/PostCard.astro');
expect(cardNode).toBeDefined();
const cardCallers = cg.getCallers(cardNode!.id);
expect(cardCallers.some((c) => c.node.filePath === 'src/pages/index.astro')).toBe(true);
// Hop 2: component template call → .ts util
const fmtNode = cg
.getNodesByKind('function')
.find((n) => n.name === 'formatDate' && n.filePath === 'src/utils/format.ts');
expect(fmtNode).toBeDefined();
const fmtCallers = cg.getCallers(fmtNode!.id);
expect(fmtCallers.some((c) => c.node.filePath === 'src/components/PostCard.astro')).toBe(true);
});
it('resolves a bare directory import (import { x } from "." / "./") to index.ts (#629)', async () => {
// `import { helper } from '.'` (or './') must map to the
// directory's index.ts before the re-export chase can run. The