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:
co-authored by
Claude Opus 4.8
parent
763ee9c825
commit
823ffd1c3d
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user