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
@@ -1373,6 +1373,7 @@ func boot(routes: RoutesBuilder) throws {
|
||||
|
||||
import { reactResolver } from '../src/resolution/frameworks/react';
|
||||
import { svelteResolver } from '../src/resolution/frameworks/svelte';
|
||||
import { astroResolver } from '../src/resolution/frameworks/astro';
|
||||
|
||||
describe('reactResolver.extract — React Router', () => {
|
||||
it('extracts a v6 <Route path element={<Comp/>}>', () => {
|
||||
@@ -1428,6 +1429,77 @@ describe('svelteResolver.extract (smoke)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('astroResolver.extract — src/pages file-based routing', () => {
|
||||
const routeNames = (filePath: string): string[] =>
|
||||
astroResolver.extract!(filePath, '').nodes.filter((n) => n.kind === 'route').map((n) => n.name);
|
||||
|
||||
it('maps index.astro to /', () => {
|
||||
expect(routeNames('src/pages/index.astro')).toEqual(['/']);
|
||||
});
|
||||
|
||||
it('maps nested index and plain pages', () => {
|
||||
expect(routeNames('src/pages/blog/index.astro')).toEqual(['/blog']);
|
||||
expect(routeNames('src/pages/about.astro')).toEqual(['/about']);
|
||||
});
|
||||
|
||||
it('converts [param] and [...rest] syntax', () => {
|
||||
expect(routeNames('src/pages/blog/[slug].astro')).toEqual(['/blog/:slug']);
|
||||
expect(routeNames('src/pages/[...path].astro')).toEqual(['/*path']);
|
||||
});
|
||||
|
||||
it('maps .ts endpoints under src/pages to routes', () => {
|
||||
expect(routeNames('src/pages/api/posts.ts')).toEqual(['/api/posts']);
|
||||
expect(routeNames('src/pages/rss.xml.js')).toEqual(['/rss.xml']);
|
||||
});
|
||||
|
||||
it('excludes underscore-prefixed segments and config files', () => {
|
||||
expect(routeNames('src/pages/_partial.astro')).toEqual([]);
|
||||
expect(routeNames('src/pages/blog/_components/Card.astro')).toEqual([]);
|
||||
expect(routeNames('src/pages/vite.config.ts')).toEqual([]);
|
||||
});
|
||||
|
||||
it('ignores .astro files outside src/pages', () => {
|
||||
expect(routeNames('src/components/Button.astro')).toEqual([]);
|
||||
expect(routeNames('docs/pages/guide.astro')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('astroResolver.resolve — Astro global and virtual modules', () => {
|
||||
const ctx = {} as never;
|
||||
const baseRef = {
|
||||
fromNodeId: 'component:a',
|
||||
line: 1,
|
||||
column: 0,
|
||||
filePath: 'src/pages/index.astro',
|
||||
language: 'astro',
|
||||
};
|
||||
|
||||
it('claims Astro.* global references as framework-provided', () => {
|
||||
const res = astroResolver.resolve(
|
||||
{ ...baseRef, referenceName: 'Astro.props', referenceKind: 'references' } as never,
|
||||
ctx
|
||||
);
|
||||
expect(res?.resolvedBy).toBe('framework');
|
||||
expect(res?.confidence).toBe(1.0);
|
||||
});
|
||||
|
||||
it('claims astro:content virtual module imports', () => {
|
||||
const res = astroResolver.resolve(
|
||||
{ ...baseRef, referenceName: 'astro:content', referenceKind: 'imports' } as never,
|
||||
ctx
|
||||
);
|
||||
expect(res?.resolvedBy).toBe('framework');
|
||||
});
|
||||
|
||||
it('leaves ordinary names alone', () => {
|
||||
const res = astroResolver.resolve(
|
||||
{ ...baseRef, referenceName: 'astrolabe', referenceKind: 'calls' } as never,
|
||||
{ getNodesByName: () => [] } as never
|
||||
);
|
||||
expect(res).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// Regression tests: commented-out and docstring route examples must NOT
|
||||
// surface as phantom route nodes. These would have failed before the
|
||||
// strip-comments wiring (the regex would happily scan comments/docstrings).
|
||||
|
||||
Reference in New Issue
Block a user