Repository navigation
feat(routing): support typed navigation destinations - #646
Conversation
|
In hindsight, I feel like we should have shipped with typed routes from the start. I'm not actually sure I love this implementation of passing objects around – it's probably fine because AI writes it, but since Void manages the route map, I'm wondering if we could type these as "Route" which is a set of strings like |
Possibly, but never too late! FWIW Next.js took more than 5 years to add typed routes.
I understand. Personally I prefer the objects way. Actually we can support both objects and route union ways, we could add a flag to the Void plugin config to generate the route unions instead of strings. Next.js does this. I'll leave the decision to you as to which combination to allow:
My recommendation would be untyped + objects + opt-in route unions. |
4d53fb1 to
68bd297
Compare
Yes please. |
Follow-up to #646: add route checking for plain URL strings, alongside untyped strings and destination objects. It's opt-in for existing apps and on by default for new apps. ```ts // void.config.ts export default defineConfig({ routing: { typedRoutes: true }, }); ``` With `routing.typedRoutes` on, URL strings in `Link`'s `href`, `router.visit()`, `router.prefetch()`, `router.flush()` and `c.rewrite()` must match one of the app's routes. Each dynamic segment accepts any text, and a query string or hash may follow. Fixed segments can be written as is (`/café menu`) or percent-encoded, as `routeUrl()` returns them (`/caf%C3%A9%20menu`). Links may also point to same-page queries and hashes and to `http://`, `https://`, `mailto:` and `tel:` URLs. Relative paths are rejected. With the flag off, the types are unchanged. ```tsx <Link href="/about">About</Link> <Link href={`/users/${user.id}?tab=activity`}>Profile</Link> <Link href={routeUrl('/users/:id', { params: { id } })}>Profile</Link> <Link href="/abuot">About</Link> // Type error <Link href={post.path as RouteUrl}>{post.title}</Link> ``` `void/routes` exports two new types. Both are `string` when the flag is off: - `RouteUrl`: the URL of one of the app's routes. `routeUrl()` returns it, and `c.rewrite()` accepts it. - `Href`: anything a link can point to. It's `RouteUrl`, a same-page query or hash, or an external URL. `Link`'s `href` and `RouteUrlInput` use it. **New apps.** `void init` (and `create-void`, which runs it) writes `routing: { typedRoutes: true }` when it scaffolds a new Pages app. Running `void init` in an existing project leaves the setting unchanged. **Also in this PR:** - **Runtime fix:** query-only links such as `?page=2` and `Link` `data` merged into a `#hash` href now stay on the current page. Before, they went to `/?page=2`. Relative URLs now resolve against the current page instead of the origin, in the shared router, the React client and all four `Link` click handlers. - **Route coverage:** - Route files with only a default export count as routes. - A trailing `/*` also accepts its bare prefix (`/docs/*` accepts `/docs`), as Hono does. - **Vue `Link` types:** it now declares the anchor and button attributes and listeners it passes through (`target`, `download`, `@click`, …). They were rejected under strict template checking even with the flag off. Notes for review: - **How the switch works:** the generated `.void/routes.d.ts` turns the check on through a `RouteTypesConfig` interface, the same augmentation pattern as `RoutePathMap`. Encoded spellings come from a generated `EncodedRoutePathMap`. Both work whether `void/routes` resolves to the generated file or to the package. - **`RewriteDestination` is unchanged.** `routing.rewrites` and `routing.fallbacks` destinations can contain `:splat` placeholders and static files. Only `c.rewrite()`'s parameter narrows. - **Vue `Link` cast:** the exported type is `DefineSetupFnComponent<LinkProps>`. The options component isn't assignable to it, so it keeps a cast. A compile-time check next to the props fails if the written-out props drift from the runtime ones. It can't live in a test, because isolatedDeclarations prevents exporting the runtime props. - **Known type limits:** - A dynamic segment also matches empty or `/`-leading text, so with a root dynamic page `//example.com` passes. Excluding that would need per-character unions. It's documented and pinned by a fixture. - Only uppercase percent-encoding, as `routeUrl()` writes it, is accepted. - **`void.config.ts` isn't watched in dev**, so changing the flag needs a dev restart or `void prepare`. The docs say so. - **Svelte `Link.svelte`** isn't template-checked, since the repo has no `svelte-check`. Its props type lives in `link-props.ts`, which the fixtures check. Validation: - `vp run check`: 33/33 tasks passed. - Type fixtures with the flag on cover the generated declarations, the package declarations, and the package types alone, plus `Link` `href` in all four adapters. Every new case was confirmed to fail against the previous behaviour. - The four adapter integration suites pass (183 tests), including a new browser case for query-only `Link` navigation. - The packed-consumer smoke test (pnpm, all four frameworks) asserts that `void init` turns on the flag. Its build and typecheck then run with checked routes. - `vp run test --base origin/main` (before the last two commits): 15,501 passed. The 1 failure was a 5s timeout in `plugin-cloudflare-v2.test.ts` under load, and it passes on its own. The later commits were verified with their focused suites, `vp run check` and the packed checks. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Allow typed route destination objects in
router.visit(),router.prefetch(),router.flush(), andc.rewrite(). Known route patterns autocomplete and dynamic routes require matching path parameters, while existing string and supportedURLinputs continue to work.Export
RouteDestinationandRouteUrlInputfromvoid/routesand serialize destinations through the same builder asrouteUrl().c.redirect()retains Hono's string/URL signature; typed redirects usec.redirect(routeUrl(...)).Coverage includes generated and package declarations, redirect/rewrite behavior, and client navigation/cache behavior across the four adapters.
Validation with the dependent Link commit applied:
vp run check: 33/33 tasks passed.vp test run: 15,379 passed, 31 skipped.