mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 22:02:49 +08:00
fix(sveltekit): route names drop (group) folders and parameter matchers (#2131)
A `(group)` directory shares a layout and is never part of the URL, and a
parameter's `=matcher` checks its value. Kept in the name, shadcn-svelte's
`src/routes/(app)/(layout)/blocks/+page.svelte` was `/(app)/(layout)/blocks`,
so no `goto('/blocks')` or `<a href="/blocks">` ever reached it and its
Screens showed no navigation at all.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
### Fixes
|
||||
|
||||
- SvelteKit routes are now named by the address a browser asks for. A `(group)` folder like `(app)` or `(marketing)` is no longer part of a route's path, and a parameter with a matcher, like `[id=integer]`, is just `:id`. So `goto('/blocks')` and `<a href="/blocks">` now reach a page that lives in `src/routes/(app)/blocks/`, where before they reached nothing and the app's screens showed no navigation. Re-index SvelteKit projects after upgrading.
|
||||
- Vue Router apps built like vue-element-admin, vue-admin-template or vben now have their routes and screens. Routes declared in a named table (`export const constantRoutes = [...]`, `const routes: RouteRecordRaw[] = [...]`), in per-module route files, or through `new Router(...)` are now read. Child routes are joined onto their parent's path, and the parent's component is the layout around them. A lazily loaded view (`() => import('@/views/dashboard/index')`) is bound to the file it names, and `this.$router.push(...)` counts as navigation. Nuxt's file-based routes now come only from a Nuxt app, so a plain Vue app's `pages/` folder no longer turns into made-up screens, and a Nuxt app at the root of its repository now gets its routes. Re-index Vue projects after upgrading.
|
||||
- In a monorepo with an Expo app next to a Next.js app, like create-t3-turbo or tamagui's starter, each router now reads only its own app. Expo Router used to read the Next.js app's files too, so `layout.tsx`, `page.tsx`, a `_components` folder and an API route showed up as screens named `/layout`, `/page` or `/_components/posts`, and a shared package's files became screens as well. Re-index such monorepos after upgrading.
|
||||
- Calls in Vue, Svelte and Astro components now follow the same rules as JavaScript and TypeScript. A `t('…')` from `useI18n()` or a `ref(…)` from `vue` used to link to an interface property or a local variable of the same name elsewhere in the project, so on halo two unrelated symbols had over a thousand made-up callers each. A call imported from a package is also no longer mistaken for a project symbol when the package is declared in a nested app's `package.json` or a Deno import map. An import through an alias CodeGraph can't follow, like SvelteKit's `$lib/…` or a nested Nuxt app's `~/…`, is still treated as project code. Re-index Vue, Svelte, Astro and JavaScript/TypeScript projects after upgrading.
|
||||
|
||||
@@ -345,7 +345,7 @@ These frameworks additionally emit **`navigates`** edges: the function that send
|
||||
| **React Router** | `<Route path component/element>` (v5 and v6) and `createBrowserRouter([{ path, element }])` | `history.push` / `replace`, `useNavigate`'s `navigate`, a loader's `redirect`, `<Link to>` / `<NavLink to>` / `<Navigate to>` / react-router-bootstrap's `<LinkContainer to>` |
|
||||
| **TanStack Router** | `createFileRoute('/posts/$postId')` (file-based) and `createRoute({ path, getParentRoute })` composed up its parent chain (code-based); `_pathless` segments, `(group)` folders, `__root` and `<Outlet/>` layouts are not addresses | `navigate({ to })`, a thrown `redirect({ to })`, `<Link to>` / `<Navigate to>` — where `to` is the route PATTERN and the values ride beside it in `params` |
|
||||
| **Vue Router** / **Nuxt** | `createRouter({ routes: [...] })` / `new Router(...)` and the route tables it's given (`export const constantRoutes = [...]`, per-module route files), with the view each entry names — a lazy `() => import(…)` bound to its file — and `children` joined onto their parent's path, the parent being the layout around them; plus Nuxt `pages/` file-based routes, `server/api/` endpoints and route middleware | `router.push` / `replace`, `this.$router.push`, Nuxt's `navigateTo`, `<router-link>` / `<RouterLink>` / `<NuxtLink>` — **by route name** (`push({ name: 'profile' })`) as well as by path |
|
||||
| **SvelteKit** | `src/routes/**/+page.svelte` (`[slug]` → `:slug`, `[[opt]]` → `:opt?`), joined to the `+page.server.js` beside it so a loader's guard belongs to its page | `goto('/x')`, `redirect(status, '/x')` from a load or form action, and the plain `<a href>` that is a link in a SvelteKit app |
|
||||
| **SvelteKit** | `src/routes/**/+page.svelte` (`[slug]` → `:slug`, `[[opt]]` → `:opt?`, `[id=matcher]` → `:id`, `(group)` folders stripped), joined to the `+page.server.js` beside it so a loader's guard belongs to its page | `goto('/x')`, `redirect(status, '/x')` from a load or form action, and the plain `<a href>` that is a link in a SvelteKit app |
|
||||
| **Angular** | `Routes` arrays (`RouterModule.forRoot` / `forChild`, `provideRouter`, a routes file's default export) with `component` or a lazy `loadComponent`; `children` and lazy `loadChildren` (an NgModule's through its routing module) joined into full paths; paths written as route constants or `$localize` strings; a route with children is a layout around the screens inside it | `router.navigate([...])`, `navigateByUrl`, a guard's `createUrlTree` / `parseUrl` — a command array, a route constant, or a component property holding one — `routerLink` / `[routerLink]` in the component's template, and `redirectTo`. Each template's child components (`<app-foo>`) are linked to the component that renders them |
|
||||
|
||||
In a repository holding several apps, each app's routes are matched only against navigation written inside that app.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* SvelteKit route names are the URL a browser asks for.
|
||||
*
|
||||
* - A `(group)` directory organizes layouts and never appears in the URL:
|
||||
* shadcn-svelte's `src/routes/(app)/(layout)/blocks/+page.svelte` is `/blocks`.
|
||||
* - `[param=matcher]` is a parameter whose value a matcher checks: `/view/:view`.
|
||||
*
|
||||
* Named with the groups, no `goto('/blocks')` or `<a href="/blocks">` ever
|
||||
* matched its page.
|
||||
*/
|
||||
import { describe, it, expect, afterAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
const roots: string[] = [];
|
||||
afterAll(() => {
|
||||
for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('SvelteKit route names', () => {
|
||||
it('drop (group) directories and parameter matchers', async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-sveltekit-groups-'));
|
||||
roots.push(root);
|
||||
const files: Record<string, string> = {
|
||||
'package.json': JSON.stringify({ name: 'site', devDependencies: { '@sveltejs/kit': '*', svelte: '*' } }),
|
||||
'src/routes/(app)/(layout)/blocks/+page.svelte': `<h1>Blocks</h1>
|
||||
`,
|
||||
'src/routes/(app)/(layout)/+page.svelte': `<script>
|
||||
import { goto } from '$app/navigation';
|
||||
</script>
|
||||
<button on:click={() => goto('/blocks')}>Blocks</button>
|
||||
<a href="/view/card">Card</a>
|
||||
`,
|
||||
'src/routes/(view)/view/[view=view]/+page.svelte': `<p>view</p>
|
||||
`,
|
||||
'src/routes/docs/[...slug=doc]/+page.svelte': `<p>doc</p>
|
||||
`,
|
||||
'src/params/view.ts': `export function match(p: string) { return p.length > 0; }
|
||||
`,
|
||||
};
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, rel), content);
|
||||
}
|
||||
const cg = await CodeGraph.init(root, { index: true });
|
||||
try {
|
||||
const routes = cg.getNodesByKind('route').map((n) => n.name).sort();
|
||||
expect(routes).toEqual(['/', '/blocks', '/docs/*slug', '/view/:view']);
|
||||
const home = cg.getNodesByKind('route').find((n) => n.name === '/')!;
|
||||
const navigates = cg
|
||||
.getOutgoingEdgesFrom(cg.getNodesInFile(home.filePath).map((n) => n.id), ['navigates' as never])
|
||||
.map((e) => cg.getNode(e.target)!.name)
|
||||
.sort();
|
||||
expect(navigates).toEqual(['/blocks', '/view/:view']);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,7 @@ guessed.
|
||||
| React Router | `frameworks/react-router.ts` | `react-router-synthesizer.ts` | `react-router.test.ts` | proshop (44 edges) |
|
||||
| TanStack Router | `frameworks/tanstack-router.ts` | `tanstack-router-synthesizer.ts` | `tanstack-router.test.ts` | TanStack examples, fastapi-template frontend |
|
||||
| Vue Router / Nuxt | `frameworks/vue-router.ts` (Nuxt file routes: `nuxtResolver` in `frameworks/vue.ts`) | `vue-router-synthesizer.ts` | `vue-router.test.ts` | vue-realworld (23 edges); vue-element-admin (62 routes), vue-admin-template (14), vben (192), halo console (34) — named tables, module files, `children` + layouts; Nuxt: mealie, elk, nuxt/movies |
|
||||
| SvelteKit | `frameworks/sveltekit-router.ts` | `sveltekit-synthesizer.ts` | `sveltekit-router.test.ts` | sveltekit-realworld (31 edges) |
|
||||
| SvelteKit | `frameworks/sveltekit-router.ts` | `sveltekit-synthesizer.ts` | `sveltekit-router.test.ts`, `sveltekit-route-names.test.ts` | sveltekit-realworld (31 edges); shadcn-svelte and skeleton (`(group)` layouts: 13 and 23 edges), svelte.dev (74), kit's test apps (47) |
|
||||
| Angular | `frameworks/angular-router.ts` | `angular-template-synthesizer.ts` | `angular-router.test.ts` | angular-realworld (31 edges, 18 renders), Ghostfolio (189 edges, 170 renders), ngx-admin (routes and renders; its menus are config) |
|
||||
|
||||
Shared machinery all seven use, in `frameworks/expo-router.ts`: `RouteTable` /
|
||||
|
||||
@@ -276,11 +276,14 @@ function filePathToSvelteKitRoute(filePath: string): string | null {
|
||||
const lastSlash = afterRoutes.lastIndexOf('/');
|
||||
const dirPath = lastSlash === -1 ? '' : afterRoutes.substring(0, lastSlash);
|
||||
|
||||
// Convert SvelteKit param syntax [param] to :param
|
||||
let route = '/' + dirPath
|
||||
.replace(/\[\.\.\.([^\]]+)\]/g, '*$1') // [...rest] -> *rest
|
||||
.replace(/\[{2}([^\]]+)\]{2}/g, ':$1?') // [[optional]] -> :optional?
|
||||
.replace(/\[([^\]]+)\]/g, ':$1'); // [param] -> :param
|
||||
// A `(group)` directory shares a layout and never appears in the URL:
|
||||
// `(app)/(layout)/blocks` is `/blocks`. A parameter's `=matcher` checks
|
||||
// its value and is no part of its name.
|
||||
const segments = dirPath.split('/').filter((seg) => seg.length > 0 && !/^\(.*\)$/.test(seg));
|
||||
let route = '/' + segments.join('/')
|
||||
.replace(/\[\.\.\.([^\]=]+)(?:=[^\]]*)?\]/g, '*$1') // [...rest] / [...rest=m] -> *rest
|
||||
.replace(/\[{2}([^\]=]+)(?:=[^\]]*)?\]{2}/g, ':$1?') // [[optional]] -> :optional?
|
||||
.replace(/\[([^\]=]+)(?:=[^\]]*)?\]/g, ':$1'); // [param] / [param=m] -> :param
|
||||
|
||||
if (route === '/') return '/';
|
||||
// Remove trailing slash
|
||||
|
||||
Reference in New Issue
Block a user