Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
286 changes: 286 additions & 0 deletions __tests__/solid-start.test.ts
Original file line number Diff line number Diff line change
@@ -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 <AppRouter root={props => <main>{props.children}</main>}><Pages /></AppRouter>}`;
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 <main/>}`);
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 <main>\n <Title>About</Title>\n <h1>About</h1>\n </main>\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 <p/>}\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 () => <p/>;\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 <p/>}',
);
write(
'src/routes/fake.tsx',
'function Decoy(){return <p/>} 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 && <Router><FileRoutes/></Router>}`,
],
[
'src/app.tsx',
`import {Router} from '@solidjs/router';import {FileRoutes} from '@solidjs/start/router';export default Router => <Router><FileRoutes/></Router>`,
],
[
'src/app.tsx',
`import {Router} from '@solidjs/router';import {FileRoutes} from '@solidjs/start/router';export default ({Router}) => <Router><FileRoutes/></Router>`,
],
[
'src/app.tsx',
`import {Router} from '@solidjs/router';import {FileRoutes} from '@solidjs/start/router';const Unused=()=> <Router><FileRoutes/></Router>; export default ()=> <p/>`,
],
['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 <Router><FileRoutes/></Router>}`,
],
[
'src/app.tsx',
`import {Router} from '@solidjs/router';import {FileRoutes} from '@solidjs/start/router';export default function App(FileRoutes){return <Router><FileRoutes/></Router>}`,
],
[
'src/app.tsx',
`import {Router} from '@solidjs/router';import {FileRoutes} from '@solidjs/start/router';export default function App(){return <Router base="/base"><FileRoutes/></Router>}`,
],
])('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 <p/>}');
await cg.sync();
expect(routeRoots(cg, routes()).get(routes()[0]!.id)!.node.name).toBe('Renamed');
write('src/app.tsx', 'export default function App(){return <p/>}');
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']]);
},
);
});
4 changes: 2 additions & 2 deletions docs/design/PLAN-application-router-coverage.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions docs/design/framework-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions site/src/content/docs/guides/framework-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading