diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45e050a04..aad2556a0 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
+- SolidStart 2 default file routes now link to exact page and HTTP handlers, including page/API coexistence, nested layouts, parameters and GET-to-HEAD fallback.
+
- 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.
diff --git a/__tests__/solid-start.test.ts b/__tests__/solid-start.test.ts
new file mode 100644
index 000000000..003d31b1e
--- /dev/null
+++ b/__tests__/solid-start.test.ts
@@ -0,0 +1,286 @@
+import { afterEach, 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 { routeRoots } from '../src/ui-server/api/route-roots';
+
+describe('SolidStart 2 default routes', () => {
+ let cg: CodeGraph | undefined;
+ let dir: string;
+ const write = (file: string, source: string) => {
+ fs.mkdirSync(path.dirname(path.join(dir, file)), { recursive: true });
+ fs.writeFileSync(path.join(dir, file), source);
+ };
+ const config = `import {defineConfig} from 'vite'; import {solidStart as start} from '@solidjs/start/config'; export default defineConfig({plugins:[start()]});`;
+ const app = `import {Router as AppRouter} from '@solidjs/router'; import {FileRoutes as Pages} from '@solidjs/start/router'; export default function App(){return {props.children}}>}`;
+ const setup = () => {
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-solid-start-'));
+ write(
+ 'package.json',
+ JSON.stringify({ dependencies: { '@solidjs/start': '2.0.4', '@solidjs/router': '0.16.3' } }),
+ );
+ write('vite.config.ts', config);
+ write('src/app.tsx', app);
+ };
+ const page = (file: string, name: string) =>
+ write(`src/routes/${file}.tsx`, `export default function ${name}(){return }`);
+ const routes = () =>
+ cg!.getNodesByKind('route').filter((n) => n.id.startsWith('route:solid-start:'));
+ afterEach(() => {
+ cg?.close();
+ cg = undefined;
+ if (dir) fs.rmSync(dir, { recursive: true, force: true });
+ });
+
+ it('indexes official page and API source fixtures with exact targets and calls', async () => {
+ setup();
+ // solidjs/solid-start@5d23efbcbb47997a70978be8b0e468df50d774a8:
+ // apps/fixtures/basic/src/routes/about.tsx; experiments/src/routes/api/hello/[name].ts.
+ // These extracted examples share a minimal app; the workspace config import uses the public package.
+ write(
+ 'src/routes/about.tsx',
+ 'import { Title } from "@solidjs/meta";\n\nexport default function About() {\n return (\n \n About\n About
\n \n );\n}\n',
+ );
+ write(
+ 'src/routes/api/hello/[name].ts',
+ 'import type { APIHandler } from "@solidjs/start/server";\n\nexport const GET: APIHandler = async ({ params }) => {\n return `Hello ${params.name}!`;\n};\n',
+ );
+ write(
+ 'src/routes/both.tsx',
+ 'function load(){return "ok"}\nexport default function Both(){return
}\nexport function GET(){return load()}\nexport const POST=()=>load();',
+ );
+ cg = await CodeGraph.init(dir, { index: true });
+ expect(
+ routes()
+ .map((n) => n.name)
+ .sort(),
+ ).toEqual([
+ '/about',
+ '/both',
+ 'GET /api/hello/:name',
+ 'GET /both',
+ 'HEAD /api/hello/:name',
+ 'HEAD /both',
+ 'POST /both',
+ ]);
+ const roots = routeRoots(cg, routes());
+ expect(roots.get(routes().find((n) => n.name === '/about')!.id)!.node.name).toBe('About');
+ for (const method of ['GET', 'HEAD']) {
+ const endpoint = routes().find((n) => n.name === `${method} /api/hello/:name`)!;
+ const target = cg.getOutgoingEdges(endpoint.id).find((e) => e.kind === 'references')!;
+ expect([cg.getNode(target.target)?.name, cg.getNode(target.target)?.filePath]).toEqual([
+ 'GET',
+ 'src/routes/api/hello/[name].ts',
+ ]);
+ }
+ const get = cg
+ .getNodesByKind('function')
+ .find((n) => n.name === 'GET' && n.filePath === 'src/routes/both.tsx')!;
+ expect(
+ cg
+ .getOutgoingEdges(get.id)
+ .some((e) => e.kind === 'calls' && cg!.getNode(e.target)?.name === 'load'),
+ ).toBe(true);
+ });
+ it('links multiline function values to their actual symbol positions', async () => {
+ setup();
+ write(
+ 'src/routes/index.tsx',
+ 'const Home =\n () => ;\nexport default Home;\nexport const GET =\n () => 1;',
+ );
+ cg = await CodeGraph.init(dir, { index: true });
+ const roots = routeRoots(cg, routes());
+ expect(
+ routes()
+ .map((route) => [route.name, roots.get(route.id)?.node.name])
+ .sort(),
+ ).toEqual([
+ ['/', 'Home'],
+ ['GET /', 'GET'],
+ ['HEAD /', 'GET'],
+ ]);
+ });
+ it('composes parameters, groups, literal dots and page-only hierarchy', async () => {
+ setup();
+ for (const [file, name] of [
+ ['index', 'Home'],
+ ['admin', 'Admin'],
+ ['admin/index', 'AdminIndex'],
+ ['admin/users/[id]', 'User'],
+ ['test(named)/child', 'TestChild'],
+ ['test', 'Test'],
+ ['(auth)/login', 'Login'],
+ ['[[id]]', 'Optional'],
+ ['[...slug]', 'CatchAll'],
+ ['_private', 'Private'],
+ ['foo.bar', 'Dot'],
+ ['api', 'ApiPage'],
+ ])
+ page(file!, name!);
+ write('src/routes/api/child.ts', 'export function GET(){return 1}');
+ cg = await CodeGraph.init(dir, { index: true });
+ expect(
+ routes()
+ .map((n) => n.name)
+ .sort(),
+ ).toEqual([
+ '/',
+ '/*slug',
+ '/:id?',
+ '/_private',
+ '/admin',
+ '/admin/users/:id',
+ '/api',
+ '/foo.bar',
+ '/login',
+ '/test',
+ '/test/child',
+ 'GET /api/child',
+ 'HEAD /api/child',
+ ]);
+ });
+ it('keeps explicit HEAD and skips optional APIs, route overrides and OPTIONS-only modules', async () => {
+ setup();
+ write(
+ 'src/routes/explicit.ts',
+ 'export function GET(){return 1}\nexport function HEAD(){return 2}\nexport function OPTIONS(){return 3}',
+ );
+ write('src/routes/options.ts', 'export function OPTIONS(){return 1}');
+ write('src/routes/type-only.ts', 'function GET(){}; export type {GET};');
+ write('src/routes/reassigned.ts', 'export function GET(){}; GET=other;');
+ page('config/child', 'ChildOfOverride');
+ write('src/routes/[[id]].ts', 'export function GET(){return 1}');
+ write(
+ 'src/routes/config.tsx',
+ 'export const route={path:"other"}; export default function Config(){return }',
+ );
+ write(
+ 'src/routes/fake.tsx',
+ 'function Decoy(){return } export default 1; export const GET=1;',
+ );
+ cg = await CodeGraph.init(dir, { index: true });
+ expect(
+ routes()
+ .map((n) => n.name)
+ .sort(),
+ ).toEqual(['GET /explicit', 'HEAD /explicit', 'OPTIONS /explicit']);
+ const head = routes().find((n) => n.name === 'HEAD /explicit')!;
+ expect(
+ cg
+ .getOutgoingEdges(head.id)
+ .filter((e) => e.kind === 'references')
+ .map((e) => cg!.getNode(e.target)?.name),
+ ).toEqual(['HEAD']);
+ });
+ it.each([
+ [
+ 'src/app.tsx',
+ `import {Router} from '@solidjs/router';import {FileRoutes} from '@solidjs/start/router';export default function App(){return false && }`,
+ ],
+ [
+ 'src/app.tsx',
+ `import {Router} from '@solidjs/router';import {FileRoutes} from '@solidjs/start/router';export default Router => `,
+ ],
+ [
+ 'src/app.tsx',
+ `import {Router} from '@solidjs/router';import {FileRoutes} from '@solidjs/start/router';export default ({Router}) => `,
+ ],
+ [
+ 'src/app.tsx',
+ `import {Router} from '@solidjs/router';import {FileRoutes} from '@solidjs/start/router';const Unused=()=> ; export default ()=> `,
+ ],
+ ['vite.config.ts', `import {solidStart} from 'other';export default {plugins:[solidStart()]}`],
+ [
+ 'vite.config.ts',
+ `import {solidStart} from '@solidjs/start/config';export default {root:'custom',plugins:[solidStart()]}`,
+ ],
+ [
+ 'vite.config.ts',
+ `import {solidStart} from '@solidjs/start/config';export default {plugins:[solidStart({routeDir:'custom'})]}`,
+ ],
+ [
+ 'vite.config.ts',
+ `import {solidStart} from '@solidjs/start/config';export default {plugins:[solidStart()],...other}`,
+ ],
+ [
+ 'vite.config.ts',
+ `import {solidStart} from '@solidjs/start/config';export default {plugins:[solidStart()],plugins:[]}`,
+ ],
+ [
+ 'src/app.tsx',
+ `import {Router} from '@solidjs/router';import {FileRoutes} from 'other';export default function App(){return }`,
+ ],
+ [
+ 'src/app.tsx',
+ `import {Router} from '@solidjs/router';import {FileRoutes} from '@solidjs/start/router';export default function App(FileRoutes){return }`,
+ ],
+ [
+ 'src/app.tsx',
+ `import {Router} from '@solidjs/router';import {FileRoutes} from '@solidjs/start/router';export default function App(){return }`,
+ ],
+ ])('does not guess unsupported registration %s (%s)', async (file, source) => {
+ setup();
+ page('index', 'Home');
+ write(file, source);
+ cg = await CodeGraph.init(dir, { index: true });
+ expect(routes()).toEqual([]);
+ });
+ it('updates unchanged parents and config after scoped edits and reopening', async () => {
+ setup();
+ page('parent', 'Parent');
+ cg = await CodeGraph.init(dir, { index: true });
+ expect(routes().map((n) => n.name)).toEqual(['/parent']);
+ cg.close();
+ cg = await CodeGraph.open(dir);
+ page('parent/child', 'Child');
+ await cg.sync({ paths: ['src/routes/parent/child.tsx'] });
+ expect(routes().map((n) => n.name)).toEqual(['/parent/child']);
+ fs.unlinkSync(path.join(dir, 'src/routes/parent/child.tsx'));
+ await cg.sync();
+ expect(routes().map((n) => n.name)).toEqual(['/parent']);
+ write('src/routes/parent.tsx', 'export default function Renamed(){return }');
+ await cg.sync();
+ expect(routeRoots(cg, routes()).get(routes()[0]!.id)!.node.name).toBe('Renamed');
+ write('src/app.tsx', 'export default function App(){return }');
+ await cg.sync({ paths: ['src/app.tsx'] });
+ expect(routes()).toEqual([]);
+ write('src/app.tsx', app);
+ await cg.sync();
+ expect(routes().map((n) => n.name)).toEqual(['/parent']);
+ write('vite.config.ts', 'export default {}');
+ await cg.sync();
+ expect(routes()).toEqual([]);
+ });
+ it.each([false, true])('detects a newly introduced framework (scoped=%s)', async (scoped) => {
+ setup();
+ write('package.json', '{}');
+ page('index', 'Home');
+ cg = await CodeGraph.init(dir, { index: true });
+ expect(routes()).toEqual([]);
+ write('package.json', JSON.stringify({ dependencies: { '@solidjs/start': '2.0.4' } }));
+ write('src/app.tsx', app + '\n');
+ await cg.sync(scoped ? { paths: ['src/app.tsx'] } : undefined);
+ expect(routes().map((n) => n.name)).toEqual(['/']);
+ });
+ it.runIf(fs.existsSync(path.resolve('dist/index.js')))(
+ 'uses fresh compiled parse and resolution workers',
+ () => {
+ setup();
+ page('index', 'Home');
+ 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').find(r=>r.id.startsWith('route:solid-start:'));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_PARSE_WORKERS: '2',
+ CODEGRAPH_PARALLEL_RESOLVE_MIN: '1',
+ CODEGRAPH_RESOLVE_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 d9088c0be..2b1538066 100644
--- a/docs/design/PLAN-application-router-coverage.md
+++ b/docs/design/PLAN-application-router-coverage.md
@@ -1,4 +1,4 @@
-Status: 8/13 — Solid Router verified; publishing step 8 before SolidStart
+Status: 9/13 — SolidStart validated; publishing step 9
- [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.
@@ -8,7 +8,7 @@ Status: 8/13 — Solid Router verified; publishing step 8 before SolidStart
- [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.
- [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.
+- [x] 9 SolidStart — pinned default pages and HTTP handlers bind exact targets; file hierarchy, page/API coexistence, scoped/reopened sync and fresh workers pass; build passes, 115 WASM focused/control tests pass, full native suite 4,542 pass / 46 skip; independent review clear.
- [ ] 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.
- [ ] 12 Waku filesystem routes — default pages, parameters, and layout exclusions for a pinned version. Gate: shared proof and `_root`/`_layout`/`_slices` controls.
diff --git a/docs/design/framework-coverage.md b/docs/design/framework-coverage.md
index f68aa366b..de8ff1966 100644
--- a/docs/design/framework-coverage.md
+++ b/docs/design/framework-coverage.md
@@ -47,6 +47,9 @@ guessed.
| 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 |
+| SolidStart | `frameworks/solid-start.ts` | — | `solid-start.test.ts` | pinned 2.0.4 About/API fixtures; page/API coexistence, file hierarchy and config sync |
+
+SolidStart supports default `src/routes/**/*.{js,jsx,ts,tsx}` with option-free `solidStart()` in a literal Vite config. Page discovery additionally requires imported `FileRoutes` directly inside the imported `Router` in the default app component. Named local default functions and method exports bind exact targets. Raw file hierarchy determines page layouts before route groups are removed; API-only descendants do not turn pages into layouts. Dots stay literal, optional page parameters and named catchalls are preserved, and GET supplies HEAD unless explicitly exported. [Official page](https://github.com/solidjs/solid-start/blob/5d23efbcbb47997a70978be8b0e468df50d774a8/apps/fixtures/basic/src/routes/about.tsx), [API fixture](https://github.com/solidjs/solid-start/blob/5d23efbcbb47997a70978be8b0e468df50d774a8/apps/fixtures/experiments/src/routes/api/hello/%5Bname%5D.ts), [route construction](https://github.com/solidjs/solid-start/blob/5d23efbcbb47997a70978be8b0e468df50d774a8/packages/start/src/config/fs-router.ts). Version 2.0.4 excludes OPTIONS-only APIs and rejects optional API parameters. Custom roots, plugin options, dynamic configuration, page `route` overrides, non-default routers, anonymous/re-exported handlers and Markdown are unsupported. No navigation is inferred.
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.
diff --git a/site/src/content/docs/guides/framework-routes.md b/site/src/content/docs/guides/framework-routes.md
index 168199a99..86994f2e8 100644
--- a/site/src/content/docs/guides/framework-routes.md
+++ b/site/src/content/docs/guides/framework-routes.md
@@ -40,6 +40,9 @@ CodeGraph detects web-framework routing files and emits `route` nodes linked by
| **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 |
+| **SolidStart** | Default file pages and HTTP-method exports; exact local targets, nested layouts, groups, parameters and GET-to-HEAD fallback |
+
+SolidStart coverage targets version 2.0.4: option-free `solidStart()` in Vite and, for pages, `FileRoutes` directly inside the default app's `Router`. A file can supply both a page and endpoints. Named local functions and constant function exports are supported. Custom roots/options, dynamic config, page route overrides, anonymous/re-exported handlers and Markdown remain unsupported. Optional parameters apply to pages only; OPTIONS-only APIs are excluded by this version's runtime.
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.
diff --git a/src/extraction/index.ts b/src/extraction/index.ts
index 03da9ea38..23bca1ba0 100644
--- a/src/extraction/index.ts
+++ b/src/extraction/index.ts
@@ -35,6 +35,7 @@ import ignore, { Ignore } from 'ignore';
import { detectFrameworks } from '../resolution/frameworks';
import { extractAngularRoutes, isAngularRegistrationFile } from '../resolution/frameworks/angular';
import { extractAnalogRoutes, isAnalogPage } from '../resolution/frameworks/analog';
+import { extractSolidStartRoutes, isSolidStartRoute } from '../resolution/frameworks/solid-start';
import type { ResolutionContext } from '../resolution/types';
import { createYielder, type MaybeYield } from '../resolution/cooperative-yield';
@@ -2374,11 +2375,14 @@ export class ExtractionOrchestrator {
const frameworks = this.ensureDetectedFrameworks();
const angular = frameworks.includes('angular') && isAngularRegistrationFile(content);
const analog = frameworks.includes('analog') && isAnalogPage(filePath);
- if (!angular && !analog) return result;
- await loadGrammarsForLanguages(['typescript', 'javascript']);
+ const solidStart = frameworks.includes('solid-start') && isSolidStartRoute(filePath);
+ if (!angular && !analog && !solidStart) return result;
+ await loadGrammarsForLanguages(solidStart ? ['typescript', 'javascript', 'tsx', 'jsx'] : ['typescript', 'javascript']);
result = materializeKernelResult(result, filePath, detectLanguage(filePath)!);
const context = this.frameworkSourceContext!;
- const extracted = angular ? extractAngularRoutes(filePath, content, context) : extractAnalogRoutes(filePath, content, context);
+ const extracted = angular ? extractAngularRoutes(filePath, content, context)
+ : analog ? extractAnalogRoutes(filePath, content, context)
+ : extractSolidStartRoutes(filePath, content, context);
result.nodes.push(...extracted.nodes);
result.unresolvedReferences.push(...extracted.references);
return result;
@@ -2893,6 +2897,16 @@ export class ExtractionOrchestrator {
: previous.includes('react-router-files');
this.detectedFrameworkNames = null;
const detected = this.ensureDetectedFrameworks(currentFiles);
+ if (detected.includes('solid-start') || this.queries.getNodesByKind('route').some(n => n.id.startsWith('route:solid-start:'))) {
+ const scope = this.scopedSyncMatcher();
+ for (const filePath of new Set([...this.queries.getAllFilePaths(), ...currentFiles])) {
+ if (!isSolidStartRoute(filePath) || filesToIndex.includes(filePath) || scope.ignores(filePath) || !fs.existsSync(path.join(this.rootDir, filePath))) continue;
+ filesToIndex.push(filePath);
+ this.conventionInvalidatedFiles.add(filePath);
+ changedFilePaths.push(filePath);
+ filesModified++;
+ }
+ }
if (detected.includes('analog') || this.queries.getNodesByKind('route').some(n => n.id.startsWith('route:analog:'))) {
const scope = this.scopedSyncMatcher();
for (const filePath of new Set([...this.queries.getAllFilePaths(), ...currentFiles])) {
diff --git a/src/resolution/frameworks/index.ts b/src/resolution/frameworks/index.ts
index bf519658d..54398f807 100644
--- a/src/resolution/frameworks/index.ts
+++ b/src/resolution/frameworks/index.ts
@@ -24,6 +24,7 @@ import { redwoodResolver } from './redwood';
import { angularResolver } from './angular';
import { analogResolver } from './analog';
import { solidRouterResolver } from './solid-router';
+import { solidStartResolver } from './solid-start';
import { djangoResolver, flaskResolver, fastapiResolver } from './python';
import { railsResolver } from './ruby';
import { springResolver } from './java';
@@ -71,6 +72,7 @@ const FRAMEWORK_RESOLVERS: FrameworkResolver[] = [
angularResolver,
analogResolver,
solidRouterResolver,
+ solidStartResolver,
// Python
djangoResolver,
flaskResolver,
diff --git a/src/resolution/frameworks/solid-start.ts b/src/resolution/frameworks/solid-start.ts
new file mode 100644
index 000000000..1d886484a
--- /dev/null
+++ b/src/resolution/frameworks/solid-start.ts
@@ -0,0 +1,412 @@
+import type { Node as SyntaxNode } from 'web-tree-sitter';
+import type { FrameworkResolver, FrameworkExtractionResult, ResolutionContext } from '../types';
+import { detectLanguage, getParser } from '../../extraction/grammars';
+import { dependsOn } from './package-deps';
+
+const ROOT = 'src/routes/';
+const METHODS = ['HEAD', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'];
+export const isSolidStartRoute = (file: string): boolean =>
+ file.startsWith(ROOT) &&
+ /\.[jt]sx?$/.test(file) &&
+ !file.endsWith('.d.ts') &&
+ !file
+ .slice(ROOT.length)
+ .split('/')
+ .some((part) => part.startsWith('.'));
+const literal = (node: SyntaxNode | null | undefined): string | null =>
+ node?.type === 'string' && !node.text.includes('\\') ? node.text.slice(1, -1) : null;
+const rawPath = (file: string): string => file.slice(ROOT.length).replace(/\.[jt]sx?$/, '');
+
+function imports(root: SyntaxNode, source: string, name: string): Set {
+ const names = new Set();
+ for (const statement of root.namedChildren) {
+ if (
+ statement.type !== 'import_statement' ||
+ literal(statement.childForFieldName('source')) !== source ||
+ statement.children.some((n) => n.type === 'type')
+ )
+ continue;
+ for (const spec of statement.descendantsOfType('import_specifier'))
+ if (
+ spec.childForFieldName('name')?.text === name &&
+ !spec.children.some((n) => n.type === 'type')
+ )
+ names.add(spec.childForFieldName('alias')?.text ?? name);
+ }
+ return names;
+}
+
+function object(node: SyntaxNode | null | undefined): Map | null {
+ if (node?.type !== 'object') return null;
+ const values = new Map();
+ for (const field of node.namedChildren) {
+ if (field.type === 'comment') continue;
+ const key = field.childForFieldName('key');
+ const name = literal(key) ?? (key?.type === 'property_identifier' ? key.text : null);
+ const value = field.childForFieldName('value');
+ if (field.type !== 'pair' || !name || !value || values.has(name)) return null;
+ values.set(name, value);
+ }
+ return values;
+}
+
+function plugin(context: ResolutionContext): boolean {
+ for (const extension of ['ts', 'js', 'mts', 'mjs']) {
+ const file = `vite.config.${extension}`;
+ const content = context.readFile(file);
+ if (content === null) continue;
+ const tree = getParser(detectLanguage(file)!)?.parse(content);
+ if (!tree) return false;
+ try {
+ const root = tree.rootNode;
+ const exported = root.namedChildren.find(
+ (n) => n.type === 'export_statement' && n.children.some((c) => c.type === 'default'),
+ );
+ let config = exported?.childForFieldName('value');
+ if (config?.type === 'call_expression') {
+ if (
+ !imports(root, 'vite', 'defineConfig').has(
+ config.childForFieldName('function')?.text ?? '',
+ )
+ )
+ return false;
+ config = config.childForFieldName('arguments')?.namedChildren[0];
+ }
+ const fields = object(config);
+ if (!fields || fields.has('root') || fields.has('base')) return false;
+ const plugins = fields.get('plugins');
+ if (plugins?.type !== 'array') return false;
+ const names = imports(root, '@solidjs/start/config', 'solidStart');
+ return plugins.namedChildren.some((call) => {
+ if (
+ call.type !== 'call_expression' ||
+ !names.has(call.childForFieldName('function')?.text ?? '')
+ )
+ return false;
+ const args =
+ call.childForFieldName('arguments')?.namedChildren.filter((n) => n.type !== 'comment') ??
+ [];
+ if (!args.length) return true;
+ const options = object(args[0]);
+ return args.length === 1 && options !== null && options.size === 0;
+ });
+ } finally {
+ tree.delete();
+ }
+ }
+ return false;
+}
+
+function fileRoutes(context: ResolutionContext): boolean {
+ for (const extension of ['tsx', 'jsx']) {
+ const file = `src/app.${extension}`;
+ const content = context.readFile(file);
+ if (content === null) continue;
+ const tree = getParser(detectLanguage(file)!)?.parse(content);
+ if (!tree) return false;
+ try {
+ const root = tree.rootNode;
+ const routers = imports(root, '@solidjs/router', 'Router');
+ const routes = imports(root, '@solidjs/start/router', 'FileRoutes');
+ const names = new Set([...routers, ...routes]);
+ const exported = root.namedChildren.find(
+ (n) => n.type === 'export_statement' && n.children.some((c) => c.type === 'default'),
+ );
+ let app = exported?.childForFieldName('declaration') ?? exported?.childForFieldName('value');
+ if (app?.type === 'identifier') {
+ const name = app.text;
+ app =
+ root.namedChildren.find(
+ (n) => n.type === 'function_declaration' && n.childForFieldName('name')?.text === name,
+ ) ??
+ root.namedChildren
+ .filter((n) => n.type === 'lexical_declaration')
+ .flatMap((n) => n.namedChildren)
+ .find((n) => n.childForFieldName('name')?.text === name)
+ ?.childForFieldName('value');
+ }
+ if (
+ !app ||
+ !['function_declaration', 'function_expression', 'arrow_function'].includes(app.type)
+ )
+ return false;
+ // A local binding with an imported name makes the JSX registration ambiguous.
+ if (
+ root
+ .descendantsOfType([
+ 'required_parameter',
+ 'optional_parameter',
+ 'variable_declarator',
+ 'function_declaration',
+ 'arrow_function',
+ ])
+ .some((n) =>
+ [...names].some(
+ (name) =>
+ (
+ n.childForFieldName('name') ??
+ n.childForFieldName('pattern') ??
+ n.childForFieldName('parameter')
+ )?.text === name ||
+ (n.childForFieldName('name') ?? n.childForFieldName('pattern'))
+ ?.descendantsOfType(['identifier', 'shorthand_property_identifier_pattern'])
+ .some((binding) => binding.text === name),
+ ),
+ )
+ )
+ return false;
+ for (const element of app.descendantsOfType('jsx_element')) {
+ let parent = element.parent;
+ while (
+ parent &&
+ parent.id !== app.id &&
+ ![
+ 'function_declaration',
+ 'function_expression',
+ 'arrow_function',
+ 'binary_expression',
+ 'ternary_expression',
+ 'if_statement',
+ 'switch_statement',
+ 'for_statement',
+ 'while_statement',
+ ].includes(parent.type)
+ )
+ parent = parent.parent;
+ if (parent?.id !== app.id) continue;
+ const opening = element.namedChildren[0];
+ if (
+ !routers.has(opening?.childForFieldName('name')?.text ?? '') ||
+ opening?.namedChildren.some(
+ (n) =>
+ n.type === 'jsx_expression' ||
+ (n.type === 'jsx_attribute' && n.namedChildren[0]?.text !== 'root'),
+ )
+ )
+ continue;
+ if (
+ element.namedChildren.some(
+ (child) =>
+ child.type === 'jsx_self_closing_element' &&
+ routes.has(child.childForFieldName('name')?.text ?? '') &&
+ child.namedChildren.length === 1,
+ )
+ )
+ return true;
+ }
+ } finally {
+ tree.delete();
+ }
+ }
+ return false;
+}
+
+type Target = { name: string; line: number; column: number; endLine: number; endColumn: number };
+type Module = { exports: Map };
+function moduleExports(file: string, content: string): Module {
+ const result: Module = { exports: new Map() };
+ const tree = getParser(detectLanguage(file)!)?.parse(content);
+ if (!tree) return result;
+ try {
+ const bindings = new Map();
+ const declaration = (node: SyntaxNode) =>
+ node.type === 'export_statement' ? node.childForFieldName('declaration') : node;
+ for (const statement of tree.rootNode.namedChildren) {
+ const node = declaration(statement);
+ if (node?.type === 'function_declaration' && node.childForFieldName('name'))
+ bindings.set(node.childForFieldName('name')!.text, node);
+ if (node?.type === 'lexical_declaration' && node.children.some((n) => n.type === 'const'))
+ for (const variable of node.namedChildren) {
+ const name = variable.childForFieldName('name');
+ const value = variable.childForFieldName('value');
+ if (
+ name?.type === 'identifier' &&
+ ['arrow_function', 'function_expression'].includes(value?.type ?? '')
+ )
+ bindings.set(name.text, variable);
+ }
+ }
+ for (const mutation of tree.rootNode.descendantsOfType([
+ 'assignment_expression',
+ 'augmented_assignment_expression',
+ 'update_expression',
+ ])) {
+ const assigned = mutation.childForFieldName('left') ?? mutation.childForFieldName('argument');
+ if (assigned?.type === 'identifier') bindings.delete(assigned.text);
+ for (const name of assigned?.descendantsOfType([
+ 'identifier',
+ 'shorthand_property_identifier_pattern',
+ ]) ?? [])
+ bindings.delete(name.text);
+ }
+ const target = (name: string): Target | null => {
+ const binding = bindings.get(name);
+ const node =
+ binding?.type === 'variable_declarator' ? binding.childForFieldName('value') : binding;
+ return node
+ ? {
+ name,
+ line: node.startPosition.row + 1,
+ column: node.startPosition.column,
+ endLine: node.endPosition.row + 1,
+ endColumn: node.endPosition.column,
+ }
+ : null;
+ };
+ for (const statement of tree.rootNode.namedChildren) {
+ if (
+ statement.type !== 'export_statement' ||
+ statement.children.some((n) => n.type === 'type')
+ )
+ continue;
+ const node = declaration(statement);
+ if (statement.children.some((n) => n.type === 'default')) {
+ const value = statement.childForFieldName('value');
+ const name =
+ node?.childForFieldName('name')?.text ?? (value?.type === 'identifier' ? value.text : '');
+ result.exports.set('default', target(name));
+ } else if (node?.type === 'function_declaration') {
+ const name = node.childForFieldName('name')?.text;
+ if (name) result.exports.set(name, target(name));
+ } else if (node?.type === 'lexical_declaration') {
+ for (const variable of node.namedChildren) {
+ const name = variable.childForFieldName('name')?.text;
+ if (name) result.exports.set(name, target(name));
+ }
+ }
+ for (const spec of statement.descendantsOfType('export_specifier')) {
+ if (spec.children.some((n) => n.type === 'type')) continue;
+ const name = spec.childForFieldName('name')?.text ?? '';
+ result.exports.set(
+ spec.childForFieldName('alias')?.text ?? name,
+ statement.childForFieldName('source') || spec.children.some((n) => n.type === 'type')
+ ? null
+ : target(name),
+ );
+ }
+ }
+ return result;
+ } finally {
+ tree.delete();
+ }
+}
+
+const states = new WeakMap<
+ ResolutionContext,
+ { active: boolean; pages: string[]; overrides: string[] }
+>();
+function project(context: ResolutionContext) {
+ const cached = states.get(context);
+ if (cached) return cached;
+ const state = { active: plugin(context), pages: [] as string[], overrides: [] as string[] };
+ if (state.active && fileRoutes(context))
+ for (const file of context.getAllFiles().filter(isSolidStartRoute)) {
+ const content = context.readFile(file);
+ if (content !== null) {
+ const exported = moduleExports(file, content).exports;
+ if (exported.has('default')) {
+ state.pages.push(file);
+ if (exported.has('route')) state.overrides.push(rawPath(file));
+ }
+ }
+ }
+ states.set(context, state);
+ return state;
+}
+
+export function extractSolidStartRoutes(
+ filePath: string,
+ content: string,
+ context: ResolutionContext,
+): FrameworkExtractionResult {
+ const result: FrameworkExtractionResult = { nodes: [], references: [] };
+ if (!isSolidStartRoute(filePath)) return result;
+ const state = project(context);
+ if (!state.active) return result;
+ const exported = moduleExports(filePath, content).exports;
+ const raw = rawPath(filePath);
+ const path =
+ (
+ '/' +
+ raw
+ .replace(/index$/, '')
+ .replace(/\[([^/]+)\]/g, (_, name: string) =>
+ name.startsWith('...')
+ ? '*' + name.slice(3)
+ : name.startsWith('[') && name.endsWith(']')
+ ? ':' + name.slice(1, -1) + '?'
+ : ':' + name,
+ )
+ .replace(/\([^)]*\)/g, '')
+ )
+ .replace(/\/+/g, '/')
+ .replace(/\/$/, '') || '/';
+ const add = (method: string, target: Target | null | undefined) => {
+ if (!target) return;
+ const name = method ? `${method} ${path}` : path;
+ const id = `route:solid-start:${filePath}:${method || 'page'}`;
+ const language = detectLanguage(filePath)!;
+ result.nodes.push({
+ id,
+ kind: 'route',
+ name,
+ qualifiedName: `${filePath}::${name}`,
+ filePath,
+ language,
+ startLine: target.line,
+ startColumn: target.column,
+ endLine: target.endLine,
+ endColumn: target.endColumn,
+ updatedAt: Date.now(),
+ });
+ result.references.push({
+ fromNodeId: id,
+ referenceName: `solid-start-target:${target.name}`,
+ referenceKind: 'references',
+ filePath,
+ language,
+ line: target.line,
+ column: target.column,
+ });
+ };
+ if (
+ !exported.has('route') &&
+ !state.overrides.some((parent) => raw.startsWith(parent + '/')) &&
+ state.pages.includes(filePath) &&
+ !state.pages.some((file) => rawPath(file).startsWith(raw + '/'))
+ )
+ add('', exported.get('default'));
+ if (
+ !raw.includes('[[') &&
+ METHODS.some((method) => method !== 'OPTIONS' && exported.has(method))
+ ) {
+ for (const method of METHODS) add(method, exported.get(method));
+ if (exported.has('GET') && !exported.has('HEAD')) add('HEAD', exported.get('GET'));
+ }
+ return result;
+}
+
+export const solidStartResolver: FrameworkResolver = {
+ name: 'solid-start',
+ languages: ['typescript', 'javascript'],
+ detect: (context) => dependsOn(context, '@solidjs/start'),
+ claimsReference: (name) => name.startsWith('solid-start-target:'),
+ resolve(ref, context) {
+ if (
+ !ref.fromNodeId.startsWith('route:solid-start:') ||
+ !ref.referenceName.startsWith('solid-start-target:')
+ )
+ return null;
+ const candidates = context
+ .getNodesInFile(ref.filePath)
+ .filter(
+ (n) =>
+ n.name === ref.referenceName.slice('solid-start-target:'.length) &&
+ ['function', 'component'].includes(n.kind) &&
+ n.startLine === ref.line,
+ );
+ return candidates.length === 1
+ ? { original: ref, targetNodeId: candidates[0]!.id, confidence: 1, resolvedBy: 'framework' }
+ : null;
+ },
+};