diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47f3e8d0f..45e050a04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### New Features
+- Solid Router registered JSX and configuration trees now link to exact components, including static lazy imports, nested paths and router bases, without duplicate React routes.
+
- Analog default file pages now link to their component classes, preserving its directory layouts, dotted paths and dynamic segments when the platform plugin and file router are registered.
- Angular Router registered route arrays now link to exact component classes, including nested children and static lazy imports; imported route edits refresh their registrations during sync.
diff --git a/__tests__/solid-router.test.ts b/__tests__/solid-router.test.ts
new file mode 100644
index 000000000..789e9b303
--- /dev/null
+++ b/__tests__/solid-router.test.ts
@@ -0,0 +1,210 @@
+import { afterEach, beforeAll, describe, expect, it } from 'vitest';
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+import { execFileSync } from 'child_process';
+import { CodeGraph } from '../src';
+import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
+import { extractSolidRoutes } from '../src/resolution/frameworks/solid-router';
+import { routeRoots } from '../src/ui-server/api/route-roots';
+
+describe('Solid Router registered declarations', () => {
+ let cg: CodeGraph | undefined;
+ let dir: string;
+ beforeAll(async () => {
+ await initGrammars();
+ await loadGrammarsForLanguages(['tsx']);
+ });
+ afterEach(() => {
+ cg?.close();
+ cg = undefined;
+ if (dir) fs.rmSync(dir, { recursive: true, force: true });
+ });
+ const imports = `import {Router, Route} from '@solidjs/router';`;
+ const names = (source: string) =>
+ extractSolidRoutes('src/app.tsx', imports + source)
+ .nodes.map((n) => n.name)
+ .sort();
+ it('composes nested paths and base while excluding layout components', () => {
+ expect(
+ names(
+ ``,
+ ),
+ ).toEqual(['/app/users', '/app/users/:id']);
+ });
+ it('accepts pathless parents, arrays and registered local config', () => {
+ expect(
+ names(
+ `const routes = [{children:[{path:['/one','/two'],component:Page}]}]; export const App = () => {routes};`,
+ ),
+ ).toEqual(['/one', '/two']);
+ });
+ it('removes a parent splat before composing children', () => {
+ expect(
+ names(
+ ``,
+ ),
+ ).toEqual(['/docs/child']);
+ });
+ it('recognizes imported aliases', () => {
+ expect(
+ extractSolidRoutes(
+ 'src/app.tsx',
+ `import {Router as R, Route as P} from '@solidjs/router'; const App=()=> ;`,
+ ).nodes.map((n) => n.name),
+ ).toEqual(['/a']);
+ });
+ it('recognizes static lazy imports in identifiers and inline config', () => {
+ const result = extractSolidRoutes(
+ 'src/app.tsx',
+ imports +
+ `import {lazy} from 'solid-js'; const Page=lazy(()=>import('./page')); const routes=[{path:'/inline',component:lazy(()=>import('./other'))}]; const App=()=> {routes};`,
+ );
+ expect(result.nodes.map((n) => n.name)).toEqual(['/named', '/inline']);
+ expect(result.references.map((n) => n.referenceName)).toEqual([
+ 'solid-lazy:./page',
+ 'solid-lazy:./other',
+ ]);
+ });
+ it.each([
+ ``,
+ `const unused = [{path:'/orphan',component:Page}];`,
+ `function App(Router) {return }`,
+ `function App(Route) {return }`,
+ ``,
+ ``,
+ ``,
+ ``,
+ `{[{path:'/x',...props,component:Page}]}`,
+ `const routes=[{path:'/x',component:Page}]; routes.push(other); {routes}`,
+ `const routes=[{path:'/x',component:Page}]; const alias=routes; alias[0].path='/other'; {routes}`,
+ `const Page=foreign(()=>import('./page')); `,
+ `function App(){const {Route}=other;return }`,
+ `const children=[]; children.push({path:'child',component:Page}); const routes=[{path:'/parent',component:Page,children:children}]; {routes}`,
+ ])('does not invent routes for unsupported declarations: %s', (source) => {
+ expect(names(source)).toEqual([]);
+ });
+ it('indexes exact imported components without duplicate React routes and syncs edits/deletions', async () => {
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-solid-'));
+ fs.mkdirSync(path.join(dir, 'src'));
+ fs.writeFileSync(
+ path.join(dir, 'package.json'),
+ JSON.stringify({ dependencies: { '@solidjs/router': '0.16.3', 'solid-js': '1.9.9' } }),
+ );
+ fs.writeFileSync(
+ path.join(dir, 'src/page.tsx'),
+ 'export default function Page(){return }',
+ );
+ fs.writeFileSync(
+ path.join(dir, 'src/other.tsx'),
+ 'export default function Page(){return }',
+ );
+ const app = path.join(dir, 'src/app.tsx');
+ fs.writeFileSync(
+ app,
+ imports +
+ `import Page from './page'; export const App=()=> ;`,
+ );
+ cg = await CodeGraph.init(dir, { index: true });
+ let routes = cg.getNodesByKind('route');
+ expect(routes.map((n) => n.name)).toEqual(['/first']);
+ expect(routeRoots(cg, routes).get(routes[0]!.id)!.node.filePath).toBe('src/page.tsx');
+ fs.writeFileSync(
+ app,
+ imports +
+ `import Page from './page'; export const App=()=> ;`,
+ );
+ await cg.sync();
+ routes = cg.getNodesByKind('route');
+ expect(routes.map((n) => n.name)).toEqual(['/second']);
+ fs.unlinkSync(app);
+ await cg.sync();
+ expect(cg.getNodesByKind('route')).toEqual([]);
+ });
+ it('indexes the pinned upstream README lazy example through exact defaults', async () => {
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-solid-real-'));
+ fs.mkdirSync(path.join(dir, 'pages'));
+ fs.writeFileSync(
+ path.join(dir, 'package.json'),
+ JSON.stringify({ dependencies: { '@solidjs/router': '0.16.3', 'solid-js': '1.9.9' } }),
+ );
+ // solidjs/solid-router@e8d3a7f719020ef01f8879a0110d2123b8597caa README.md, lazy-load example.
+ fs.writeFileSync(
+ path.join(dir, 'app.tsx'),
+ `import { lazy } from "solid-js";
+import { render } from "solid-js/web";
+import { Router, Route } from "@solidjs/router";
+const Users = lazy(() => import("./pages/Users"));
+const Home = lazy(() => import("./pages/Home"));
+const App = (props) => (<>My Site with lots of pages
{props.children}>);
+render(() => (),document.getElementById("app"));`,
+ );
+ fs.writeFileSync(
+ path.join(dir, 'pages/Home.tsx'),
+ 'export default function Home(){return }',
+ );
+ fs.writeFileSync(
+ path.join(dir, 'pages/Users.tsx'),
+ 'export default function Users(){return }',
+ );
+ cg = await CodeGraph.init(dir, { index: true });
+ const routes = cg.getNodesByKind('route');
+ expect(routes.map((n) => n.name).sort()).toEqual(['/', '/users']);
+ const roots = routeRoots(cg, routes);
+ expect(routes.map((n) => [n.name, roots.get(n.id)!.node.name]).sort()).toEqual([
+ ['/', 'Home'],
+ ['/users', 'Users'],
+ ]);
+ cg.close();
+ cg = await CodeGraph.open(dir);
+ fs.writeFileSync(path.join(dir, 'pages/Home.tsx'), 'function Decoy(){}; export default 1;');
+ await cg.sync();
+ const home = cg.getNodesByKind('route').find((n) => n.name === '/')!;
+ expect(cg.getOutgoingEdges(home.id).filter((e) => e.kind === 'references')).toEqual([]);
+ });
+ it('detects Solid introduced by scoped sync and adds new registered routes', async () => {
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-solid-sync-'));
+ fs.writeFileSync(path.join(dir, 'package.json'), '{}');
+ fs.writeFileSync(path.join(dir, 'seed.ts'), 'export const seed=1;');
+ cg = await CodeGraph.init(dir, { index: true });
+ fs.writeFileSync(
+ path.join(dir, 'package.json'),
+ JSON.stringify({ dependencies: { '@solidjs/router': '0.16.3' } }),
+ );
+ fs.writeFileSync(path.join(dir, 'Home.tsx'), 'export default function Home(){return }');
+ fs.writeFileSync(
+ path.join(dir, 'app.tsx'),
+ imports +
+ `import Home from './Home'; export const App=()=> ;`,
+ );
+ await cg.sync({ paths: ['app.tsx', 'Home.tsx', 'package.json'] });
+ const routes = cg.getNodesByKind('route');
+ expect(routes.map((n) => n.name)).toEqual(['/added']);
+ expect(routeRoots(cg, routes).get(routes[0]!.id)!.node.name).toBe('Home');
+ });
+ it('extracts and resolves lazy components in fresh compiled workers', () => {
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-solid-workers-'));
+ fs.writeFileSync(
+ path.join(dir, 'package.json'),
+ JSON.stringify({ dependencies: { '@solidjs/router': '0.16.3' } }),
+ );
+ fs.writeFileSync(path.join(dir, 'Home.tsx'), 'export default function Home(){return }');
+ fs.writeFileSync(
+ path.join(dir, 'app.tsx'),
+ imports +
+ `import {lazy} from 'solid-js'; const Home=lazy(()=>import('./Home')); export const App=()=> ;`,
+ );
+ const script = `const {CodeGraph}=require(${JSON.stringify(path.resolve('dist/index.js'))});(async()=>{const cg=await CodeGraph.init(${JSON.stringify(dir)},{index:true});const r=cg.getNodesByKind('route')[0];console.log(JSON.stringify([r?.name,r&&cg.getOutgoingEdges(r.id).filter(e=>e.kind==='references').map(e=>cg.getNode(e.target)?.name)]));cg.close();})().catch(e=>{console.error(e);process.exit(1)});`;
+ const output = execFileSync(process.execPath, ['-e', script], {
+ encoding: 'utf8',
+ timeout: 60000,
+ env: {
+ ...process.env,
+ CODEGRAPH_PARALLEL_RESOLVE_MIN: '1',
+ CODEGRAPH_RESOLVE_WORKERS: '2',
+ CODEGRAPH_PARSE_WORKERS: '2',
+ },
+ });
+ expect(JSON.parse(output.trim().split('\n').at(-1)!)).toEqual(['/', ['Home']]);
+ });
+});
diff --git a/docs/design/PLAN-application-router-coverage.md b/docs/design/PLAN-application-router-coverage.md
index 53aa61e47..d9088c0be 100644
--- a/docs/design/PLAN-application-router-coverage.md
+++ b/docs/design/PLAN-application-router-coverage.md
@@ -1,4 +1,4 @@
-Status: 7/13 — Analog verified; publishing step 7 before Solid Router
+Status: 8/13 — Solid Router verified; publishing step 8 before SolidStart
- [x] 1 React Router framework mode — seven official-fixture pages bind exact components; nested index/navigation, config/module sync and fresh compiled workers verified; build passes, 112 WASM focused/control tests pass, full native suite 4,353 pass / 46 skip; independent review clear.
- [x] 2 TanStack Start server routes — literal method tables and `createHandlers` bind handlers/calls; page/API coexistence and full/scoped sync verified; build passes, 62 WASM focused/control tests pass, full native suite 4,375 pass / 46 skip; independent review clear.
@@ -7,7 +7,7 @@ Status: 7/13 — Analog verified; publishing step 7 before Solid Router
- [x] 5 RedwoodSDK — registered literal trees, prefixes and method tables bind exact handlers; JSX evidence classifies pages, interrupters/ambiguous declarations excluded; handler edit/delete and new-framework sync pass; build passes, 91 WASM tests pass, full native suite 4,459 pass / 46 skip; independent review clear.
- [x] 6 Angular Router — registered arrays, nested children and static lazy imports bind exact classes; imported table add/edit/delete after reopening and fresh workers pass; build passes, 117 WASM focused/control tests pass, full native suite 4,484 pass / 46 skip; independent review clear.
- [x] 7 Analog — registered default pages bind exact classes with directory-based layouts and dot/parameter conventions; config/file and reopened/scoped sync plus fresh workers pass; build passes, 133 WASM focused/control tests pass, full native suite 4,500 pass / 46 skip; independent review clear.
-- [ ] 8 Solid Router — imported JSX/config declarations, nested path composition, and router base. Gate: shared proof, nested matching semantics, component links, and unrelated JSX negatives.
+- [x] 8 Solid Router — registered JSX/config and static lazy imports bind exact components; nested bases/splats, mutations, scoped introduction and fresh workers pass; build passes, 115 WASM focused/control tests pass, full native suite 4,523 pass / 46 skip; independent review clear.
- [ ] 9 SolidStart — pinned-version file routes, default page exports, and HTTP-method exports. Gate: shared proof, page/API coexistence, layouts, and dynamic parameters.
- [ ] 10 Qwik City — default file routes, page components, and method-specific endpoint exports. Gate: shared proof, parameters, layouts, and `onRequest`/middleware exclusions.
- [ ] 11 Vike — default `+Page` conventions and literal `+route` overrides. Gate: shared proof, parameters, exact component links, and no fallback route when an unsupported override changes routing.
diff --git a/docs/design/framework-coverage.md b/docs/design/framework-coverage.md
index aa38d2c5e..f68aa366b 100644
--- a/docs/design/framework-coverage.md
+++ b/docs/design/framework-coverage.md
@@ -46,6 +46,9 @@ guessed.
| RedwoodSDK | `frameworks/redwood.ts` | — | `redwood-routes.test.ts` | pinned 1.7.3 typed-routes worker; exact page and API roots |
| Angular Router | `frameworks/angular.ts` | — | `angular-routes.test.ts` | pinned official tutorial registration; exact class roots and imported-array sync |
| Analog | `frameworks/analog.ts` | — | `analog-routes.test.ts` | pinned 2.7.1 sign-up page; filename/layout and registration sync controls |
+| Solid Router | `frameworks/solid-router.ts` | — | `solid-router.test.ts` | pinned 0.16.3 README lazy example; exact component roots, nested paths and fresh workers |
+
+Solid Router reads imported `Router`/`Route` JSX and registered literal or module-constant config trees. It composes bases and nested paths, removes parent splats, and emits only leaf pages. Components bind through imports or same-file declarations; static `solid-js` lazy imports bind named default exports. [Pinned example](https://github.com/solidjs/solid-router/blob/e8d3a7f719020ef01f8879a0110d2123b8597caa/README.md), [path composition](https://github.com/solidjs/solid-router/blob/e8d3a7f719020ef01f8879a0110d2123b8597caa/src/utils.ts). Tests cover shadowed imports, mutated tables, exact targets, edit/delete sync and fresh workers. Other router variants, cross-file config tables, local function config bindings, dynamic declarations, spreads, inline/anonymous components and lazy re-exports remain unsupported. No navigation is inferred.
Analog recognizes `src/app/pages/**/*.page.ts` named default classes when a root Vite config registers the platform plugin and source registers option-free `provideFileRouter()`. Directory hierarchy determines layouts before dots become URL separators; index/pathless names, parameters and catchalls follow the pinned conventions. [Official page fixture](https://github.com/analogjs/analog/blob/0896a7eaaa2acf26443ca184bc1dd9aa1a06f4d6/apps/analog-app/src/app/pages/%28auth%29/sign-up.page.ts), [route construction](https://github.com/analogjs/analog/blob/0896a7eaaa2acf26443ca184bc1dd9aa1a06f4d6/packages/router/src/lib/routes.ts). Fresh workers and config/file add/edit/delete refresh existing pages, including after reopening. Custom roots, extra route directories, `app/routes`, metadata overrides, router options, optional catchalls, Markdown, anonymous defaults and re-exports remain unsupported. No navigation is inferred.
diff --git a/site/src/content/docs/guides/framework-routes.md b/site/src/content/docs/guides/framework-routes.md
index cf6070ab9..168199a99 100644
--- a/site/src/content/docs/guides/framework-routes.md
+++ b/site/src/content/docs/guides/framework-routes.md
@@ -39,6 +39,9 @@ CodeGraph detects web-framework routing files and emits `route` nodes linked by
| **RedwoodSDK** | Registered `defineApp` trees with `route`, `index`, `render`, `layout`, `prefix` and standard method tables; exact handlers and JSX page classification |
| **Angular Router** | `provideRouter` / `RouterModule.forRoot` arrays, nested children, relative component imports and static lazy components/route arrays/NgModules |
| **Analog** | Registered default `src/app/pages/**/*.page.ts` pages, linked to named default classes; directory layouts, dot paths, index/pathless segments and parameters |
+| **Solid Router** | Imported `Router`/`Route` JSX and registered literal configuration, nested paths, path arrays and bases; imported/local components and static lazy defaults |
+
+Solid Router emits leaf routes, preserving nested path composition even when a child begins with `/`. Parent components and the router root remain layouts. Cross-file configuration, other router variants, dynamic/spread declarations, inline/anonymous components and lazy re-exports are unsupported.
Route resolution is automatic — there's nothing to configure. If a framework file is recognized, its routes appear in the graph after the next index or sync.
diff --git a/src/index.ts b/src/index.ts
index dda95a202..a42bfa028 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -579,7 +579,7 @@ export class CodeGraph {
if (result.success && result.filesIndexed > 0) {
const tReinit = Date.now();
this.resolver.initialize();
- if (this.queries.getNodesByKind('route').some(n => /^route:(react-router|redwood):/.test(n.id)))
+ if (this.queries.getNodesByKind('route').some(n => /^route:(react-router|redwood|solid):/.test(n.id)))
await loadGrammarsForLanguages(['typescript', 'javascript', 'tsx', 'jsx']);
// Cross-file finalization (e.g. NestJS RouterModule prefixes). Runs
// before resolution so updated names show up in subsequent reads.
@@ -834,7 +834,7 @@ export class CodeGraph {
// (regex over *.module.ts only).
if (result.filesAdded > 0 || result.filesModified > 0) {
this.resolver.initialize();
- if (this.queries.getNodesByKind('route').some(n => /^route:(react-router|redwood):/.test(n.id)))
+ if (this.queries.getNodesByKind('route').some(n => /^route:(react-router|redwood|solid):/.test(n.id)))
await loadGrammarsForLanguages(['typescript', 'javascript', 'tsx', 'jsx']);
this.resolver.runPostExtract();
} else if (result.filesRemoved > 0) {
diff --git a/src/resolution/frameworks/index.ts b/src/resolution/frameworks/index.ts
index e788eaf34..bf519658d 100644
--- a/src/resolution/frameworks/index.ts
+++ b/src/resolution/frameworks/index.ts
@@ -23,6 +23,7 @@ import { astroResolver } from './astro';
import { redwoodResolver } from './redwood';
import { angularResolver } from './angular';
import { analogResolver } from './analog';
+import { solidRouterResolver } from './solid-router';
import { djangoResolver, flaskResolver, fastapiResolver } from './python';
import { railsResolver } from './ruby';
import { springResolver } from './java';
@@ -69,6 +70,7 @@ const FRAMEWORK_RESOLVERS: FrameworkResolver[] = [
redwoodResolver,
angularResolver,
analogResolver,
+ solidRouterResolver,
// Python
djangoResolver,
flaskResolver,
diff --git a/src/resolution/frameworks/react.ts b/src/resolution/frameworks/react.ts
index 28776cddb..934ddf461 100644
--- a/src/resolution/frameworks/react.ts
+++ b/src/resolution/frameworks/react.ts
@@ -8,6 +8,7 @@
import { Node } from '../../types';
import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
import { dependsOn } from './package-deps';
+import { detectLanguage, getParser } from '../../extraction/grammars';
export const reactResolver: FrameworkResolver = {
name: 'react',
@@ -103,8 +104,16 @@ export const reactResolver: FrameworkResolver = {
// and element={...} contains a nested `>`, so scan a window after each
//
+ statement.type === 'import_statement' &&
+ statement.childForFieldName('source')?.text.slice(1, -1) === '@solidjs/router' &&
+ statement.descendantsOfType('import_specifier').some(spec =>
+ (spec.childForFieldName('alias') ?? spec.childForFieldName('name'))?.text === 'Route'));
+ tree?.delete();
let routeMatch: RegExpExecArray | null;
- while ((routeMatch = routeTagRegex.exec(content)) !== null) {
+ while (!foreignRoute && (routeMatch = routeTagRegex.exec(content)) !== null) {
const window = content.slice(routeMatch.index, routeMatch.index + 400);
const pathMatch = window.match(/\bpath\s*=\s*["']([^"']+)["']/);
if (!pathMatch) continue; // index/layout routes without a path
diff --git a/src/resolution/frameworks/solid-router.ts b/src/resolution/frameworks/solid-router.ts
new file mode 100644
index 000000000..fbef52b5c
--- /dev/null
+++ b/src/resolution/frameworks/solid-router.ts
@@ -0,0 +1,349 @@
+import type { Node as SyntaxNode } from 'web-tree-sitter';
+import type { FrameworkResolver, FrameworkExtractionResult } from '../types';
+import { detectLanguage, getParser } from '../../extraction/grammars';
+import { resolveImportPath, resolveViaImport } from '../import-resolver';
+import { dependsOn } from './package-deps';
+
+const unwrap = (raw: SyntaxNode | null | undefined): SyntaxNode | null => {
+ let node = raw ?? null;
+ while (
+ node &&
+ [
+ 'parenthesized_expression',
+ 'as_expression',
+ 'satisfies_expression',
+ 'jsx_expression',
+ ].includes(node.type)
+ )
+ node = node.namedChildren[0] ?? null;
+ return node;
+};
+const literal = (node: SyntaxNode | null | undefined): string | null =>
+ node?.type === 'string' && !node.text.includes('\\') ? node.text.slice(1, -1) : null;
+const join = (base: string, path: string): string =>
+ ('/' + base.replace(/\*.*$/, '').replace(/^\/+|\/+$/g, '') + '/' + path.replace(/^\/+|\/+$/g, ''))
+ .replace(/\/+/g, '/')
+ .replace(/\/$/, '') || '/';
+
+/** Only children of an imported Router are route declarations. */
+export function extractSolidRoutes(filePath: string, content: string): FrameworkExtractionResult {
+ const result: FrameworkExtractionResult = { nodes: [], references: [] };
+ if (!content.includes('@solidjs/router')) return result;
+ const language = detectLanguage(filePath)!;
+ const parser = getParser(language);
+ if (!parser) throw new Error(`Solid Router extraction requires the ${language} grammar`);
+ const tree = parser.parse(content);
+ if (!tree) return result;
+ try {
+ const helpers = new Map();
+ const bindings = new Map();
+ for (const statement of tree.rootNode.namedChildren) {
+ if (
+ statement.type === 'import_statement' &&
+ !statement.children.some((n) => n.type === 'type')
+ ) {
+ const source = literal(statement.childForFieldName('source'));
+ for (const spec of statement.descendantsOfType('import_specifier')) {
+ const name = spec.childForFieldName('name')?.text;
+ if (
+ name &&
+ ((source === '@solidjs/router' && ['Route', 'Router'].includes(name)) ||
+ (source === 'solid-js' && name === 'lazy')) &&
+ !spec.children.some((n) => n.type === 'type')
+ )
+ helpers.set(spec.childForFieldName('alias')?.text ?? name, name);
+ }
+ }
+ const declaration =
+ statement.type === 'export_statement'
+ ? statement.childForFieldName('declaration')
+ : statement;
+ if (
+ declaration?.type === 'lexical_declaration' &&
+ declaration.children.some((n) => n.type === 'const')
+ )
+ for (const variable of declaration.namedChildren) {
+ const name = variable.childForFieldName('name');
+ const value = variable.childForFieldName('value');
+ if (name?.type === 'identifier' && value) bindings.set(name.text, value);
+ }
+ }
+ const mutated = new Set();
+ for (const expression of tree.rootNode.descendantsOfType([
+ 'assignment_expression',
+ 'augmented_assignment_expression',
+ 'update_expression',
+ 'call_expression',
+ ])) {
+ let receiver =
+ expression.type === 'call_expression'
+ ? expression.childForFieldName('function')
+ : (expression.childForFieldName('left') ?? expression.childForFieldName('argument'));
+ if (
+ expression.type === 'call_expression' &&
+ !['member_expression', 'subscript_expression'].includes(receiver?.type ?? '')
+ )
+ continue;
+ while (receiver && ['member_expression', 'subscript_expression'].includes(receiver.type))
+ receiver = receiver.childForFieldName('object');
+ if (receiver?.type === 'identifier') mutated.add(receiver.text);
+ }
+ for (let changed = true; changed;) {
+ changed = false;
+ for (const [name, raw] of bindings) {
+ const value = unwrap(raw);
+ if (value?.type === 'identifier' && (mutated.has(name) || mutated.has(value.text)))
+ for (const alias of [name, value.text])
+ if (!mutated.has(alias)) {
+ mutated.add(alias);
+ changed = true;
+ }
+ }
+ }
+ const shadowed = (name: string, site: SyntaxNode): boolean => {
+ for (let scope = site.parent; scope && scope.type !== 'program'; scope = scope.parent) {
+ const params =
+ scope.childForFieldName('parameters') ?? scope.childForFieldName('parameter');
+ if (
+ params &&
+ [
+ params,
+ ...params.descendantsOfType(['identifier', 'shorthand_property_identifier_pattern']),
+ ].some((n) => n.text === name)
+ )
+ return true;
+ if (scope.type === 'statement_block')
+ for (const child of scope.namedChildren) {
+ if (
+ ['function_declaration', 'class_declaration'].includes(child.type) &&
+ child.childForFieldName('name')?.text === name
+ )
+ return true;
+ if (
+ ['lexical_declaration', 'variable_declaration'].includes(child.type) &&
+ child.descendantsOfType('variable_declarator').some((n) => {
+ const pattern = n.childForFieldName('name');
+ return (
+ pattern &&
+ [
+ pattern,
+ ...pattern.descendantsOfType([
+ 'identifier',
+ 'shorthand_property_identifier_pattern',
+ ]),
+ ].some((binding) => binding.text === name)
+ );
+ })
+ )
+ return true;
+ }
+ }
+ return false;
+ };
+ const resolve = (
+ raw: SyntaxNode | null | undefined,
+ seen = new Set(),
+ ): SyntaxNode | null => {
+ const node = unwrap(raw);
+ if (node?.type !== 'identifier' || !bindings.has(node.text)) return node;
+ if (seen.has(node.text) || mutated.has(node.text) || shadowed(node.text, node)) return null;
+ seen.add(node.text);
+ return resolve(bindings.get(node.text), seen);
+ };
+ const fields = (node: SyntaxNode): Map | null => {
+ const map = new Map();
+ for (const child of node.namedChildren) {
+ if (child.type === 'comment') continue;
+ const key = child.childForFieldName('key');
+ const name = key?.type === 'property_identifier' ? key.text : literal(key);
+ const value = child.childForFieldName('value');
+ if (!name || !value || map.has(name)) return null;
+ map.set(name, value);
+ }
+ return map;
+ };
+ const tag = (node: SyntaxNode): SyntaxNode | undefined =>
+ node.type === 'jsx_element'
+ ? node.namedChildren.find((n) => n.type === 'jsx_opening_element')
+ : node.type === 'jsx_self_closing_element'
+ ? node
+ : undefined;
+ const attributes = (opening: SyntaxNode): Map | null => {
+ const map = new Map();
+ for (const child of opening.namedChildren.slice(1)) {
+ if (child.type !== 'jsx_attribute') return null;
+ const name = child.namedChildren[0]?.text;
+ const value = child.namedChildren[1];
+ if (!name || !value || map.has(name)) return null;
+ map.set(name, value);
+ }
+ return map;
+ };
+ const children = (node: SyntaxNode): SyntaxNode[] =>
+ node.type === 'jsx_self_closing_element'
+ ? []
+ : node.namedChildren.filter(
+ (n) =>
+ !['jsx_opening_element', 'jsx_closing_element', 'jsx_text', 'comment'].includes(
+ n.type,
+ ),
+ );
+ const visit = (raw: SyntaxNode | null | undefined, base: string, depth = 0): void => {
+ if (depth > 32) return;
+ const node = resolve(raw);
+ if (!node) return;
+ if (node.type === 'array' || node.type === 'jsx_fragment') {
+ for (const child of node.type === 'array' ? node.namedChildren : children(node))
+ visit(child, base, depth + 1);
+ return;
+ }
+ const opening = tag(node);
+ const name = opening?.childForFieldName('name')?.text;
+ if (opening && (!name || helpers.get(name) !== 'Route' || shadowed(name, opening))) return;
+ const props = opening ? attributes(opening) : node.type === 'object' ? fields(node) : null;
+ if (!props) return;
+ const pathNode = props.has('path') ? resolve(props.get('path')) : null;
+ const paths = !props.has('path')
+ ? ['']
+ : pathNode?.type === 'array'
+ ? pathNode.namedChildren.map(literal)
+ : [literal(pathNode)];
+ if (paths.some((p) => p === null)) return;
+ const descendants = opening
+ ? children(node)
+ : props.has('children')
+ ? [props.get('children')!]
+ : [];
+ const nested = descendants.filter((n) => {
+ const value = resolve(n);
+ return !(value?.type === 'array' && value.namedChildren.length === 0);
+ });
+ for (const part of paths) {
+ const routePath = join(base, part!);
+ if (nested.length) {
+ for (const child of nested) visit(child, routePath, depth + 1);
+ continue;
+ }
+ const component = unwrap(props.get('component'));
+ if (!component) continue;
+ let referenceName: string | null =
+ component.type === 'identifier' &&
+ !shadowed(component.text, component) &&
+ !mutated.has(component.text)
+ ? 'solid-component:' + component.text
+ : null;
+ const value = resolve(component);
+ if (value?.type === 'call_expression') {
+ referenceName = null;
+ const fn = value.childForFieldName('function');
+ const args = value.childForFieldName('arguments')?.namedChildren;
+ const callback = args?.length === 1 ? args[0] : null;
+ const body = unwrap(callback?.childForFieldName('body'));
+ const target =
+ body?.type === 'call_expression' &&
+ body.childForFieldName('function')?.type === 'import'
+ ? body.childForFieldName('arguments')?.namedChildren
+ : null;
+ const source = target?.length === 1 ? literal(target[0]) : null;
+ if (
+ fn?.type === 'identifier' &&
+ helpers.get(fn.text) === 'lazy' &&
+ !shadowed(fn.text, fn) &&
+ callback?.type === 'arrow_function' &&
+ source?.startsWith('.')
+ )
+ referenceName = 'solid-lazy:' + source;
+ }
+ if (!referenceName) continue;
+ const id = `route:solid:${filePath}:${node.startIndex}:${routePath}`;
+ if (result.nodes.some((n) => n.id === id)) continue;
+ result.nodes.push({
+ id,
+ kind: 'route',
+ name: routePath,
+ qualifiedName: `${filePath}::${routePath}`,
+ filePath,
+ language,
+ startLine: node.startPosition.row + 1,
+ endLine: node.endPosition.row + 1,
+ startColumn: node.startPosition.column,
+ endColumn: node.endPosition.column,
+ updatedAt: Date.now(),
+ });
+ result.references.push({
+ fromNodeId: id,
+ referenceName,
+ referenceKind: 'references',
+ filePath,
+ language,
+ line: component.startPosition.row + 1,
+ column: component.startPosition.column,
+ });
+ }
+ };
+ for (const node of tree.rootNode.descendantsOfType('jsx_element')) {
+ const opening = tag(node)!;
+ const name = opening.childForFieldName('name')?.text;
+ if (!name || helpers.get(name) !== 'Router' || shadowed(name, opening)) continue;
+ const props = attributes(opening);
+ if (!props) continue;
+ const base = props.has('base') ? literal(resolve(props.get('base'))) : '';
+ if (base === null) continue;
+ for (const child of children(node)) visit(child, base);
+ }
+ return result;
+ } finally {
+ tree.delete();
+ }
+}
+
+export const solidRouterResolver: FrameworkResolver = {
+ name: 'solid-router',
+ languages: ['typescript', 'javascript', 'tsx', 'jsx'],
+ detect: (context) => dependsOn(context, '@solidjs/router'),
+ extract: extractSolidRoutes,
+ claimsReference: (name) => name.startsWith('solid-component:') || name.startsWith('solid-lazy:'),
+ resolve(ref, context) {
+ if (ref.referenceName.startsWith('solid-lazy:')) {
+ const file = resolveImportPath(
+ ref.referenceName.slice('solid-lazy:'.length),
+ ref.filePath,
+ ref.language,
+ context,
+ );
+ const content = file ? context.readFile(file) : null;
+ const tree =
+ file && content !== null ? getParser(detectLanguage(file))?.parse(content) : null;
+ if (!tree || !file) return null;
+ try {
+ const exported = tree.rootNode.namedChildren.find(
+ (n) => n.type === 'export_statement' && n.children.some((c) => c.type === 'default'),
+ );
+ const declaration = exported?.childForFieldName('declaration');
+ const value = exported?.childForFieldName('value');
+ const name =
+ declaration?.childForFieldName('name')?.text ??
+ (value?.type === 'identifier' ? value.text : null);
+ if (!name) return null;
+ const targets = context
+ .getNodesInFile(file)
+ .filter((n) => ['function', 'component'].includes(n.kind) && n.name === name);
+ return targets.length === 1
+ ? { original: ref, targetNodeId: targets[0]!.id, confidence: 1, resolvedBy: 'framework' }
+ : null;
+ } finally {
+ tree.delete();
+ }
+ }
+ if (!ref.referenceName.startsWith('solid-component:')) return null;
+ const name = ref.referenceName.slice('solid-component:'.length);
+ const imported = resolveViaImport({ ...ref, referenceName: name }, context);
+ if (imported) return { ...imported, original: ref };
+ const targets = context
+ .getNodesInFile(ref.filePath)
+ .filter((n) => n.name === name && ['function', 'component'].includes(n.kind));
+ return targets.length === 1
+ ? { original: ref, targetNodeId: targets[0]!.id, confidence: 1, resolvedBy: 'framework' }
+ : null;
+ },
+};