@rangojs/router 0.0.0-experimental.e9c0b2f2 → 0.0.0-experimental.ea9f40f2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -10
- package/README.md +289 -938
- package/dist/bin/rango.js +271 -46
- package/dist/vite/index.js +673 -193
- package/package.json +10 -8
- package/skills/api-client/SKILL.md +1 -1
- package/skills/breadcrumbs/SKILL.md +31 -14
- package/skills/cache-guide/SKILL.md +5 -2
- package/skills/caching/SKILL.md +59 -4
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/debug-manifest/SKILL.md +1 -1
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +9 -1
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +8 -8
- package/skills/hooks/SKILL.md +54 -892
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +4 -4
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +46 -14
- package/skills/layout/SKILL.md +27 -10
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +23 -1
- package/skills/middleware/SKILL.md +7 -3
- package/skills/migrate-nextjs/SKILL.md +167 -6
- package/skills/migrate-react-router/SKILL.md +59 -677
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/observability/SKILL.md +9 -1
- package/skills/parallel/SKILL.md +23 -4
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +28 -18
- package/skills/rango/SKILL.md +84 -25
- package/skills/response-routes/SKILL.md +15 -1
- package/skills/route/SKILL.md +71 -4
- package/skills/router-setup/SKILL.md +14 -3
- package/skills/scripts/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +3 -2
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +1 -1
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/handles.md +4 -2
- package/skills/testing/render-handler.md +15 -14
- package/skills/testing/reverse-and-types.md +8 -7
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -919
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +30 -3
- package/skills/vercel/SKILL.md +1 -1
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/browser/event-controller.ts +62 -10
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +65 -16
- package/src/browser/navigation-client.ts +32 -2
- package/src/browser/navigation-store.ts +128 -14
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +76 -17
- package/src/browser/prefetch/cache.ts +51 -11
- package/src/browser/prefetch/fetch.ts +59 -21
- package/src/browser/prefetch/queue.ts +19 -4
- package/src/browser/react/Link.tsx +13 -3
- package/src/browser/react/NavigationProvider.tsx +108 -4
- package/src/browser/response-adapter.ts +38 -9
- package/src/browser/rsc-router.tsx +54 -4
- package/src/browser/scroll-restoration.ts +7 -5
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +22 -10
- package/src/browser/types.ts +54 -1
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/index.ts +3 -1
- package/src/build/route-trie.ts +35 -7
- package/src/build/route-types/include-resolution.ts +347 -47
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-runtime.ts +262 -71
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +243 -20
- package/src/cache/document-cache.ts +54 -21
- package/src/cache/index.ts +1 -0
- package/src/cache/memory-segment-store.ts +110 -3
- package/src/cache/profile-registry.ts +15 -0
- package/src/cache/read-through-swr.ts +15 -1
- package/src/cache/segment-codec.ts +4 -4
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/types.ts +158 -0
- package/src/cache/vercel/vercel-cache-store.ts +401 -124
- package/src/client.rsc.tsx +0 -3
- package/src/client.tsx +0 -3
- package/src/cloudflare/tracing.ts +7 -8
- package/src/defer.ts +11 -22
- package/src/handle.ts +37 -15
- package/src/handles/MetaTags.tsx +16 -82
- package/src/handles/breadcrumbs.ts +12 -14
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +7 -8
- package/src/handles/meta.ts +7 -44
- package/src/host/errors.ts +15 -0
- package/src/host/index.ts +1 -0
- package/src/index.rsc.ts +8 -2
- package/src/index.ts +19 -13
- package/src/internal-debug.ts +11 -8
- package/src/prerender.ts +17 -4
- package/src/redirect-origin.ts +14 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/route-content-wrapper.tsx +12 -5
- package/src/route-definition/dsl-helpers.ts +21 -32
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +43 -43
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/resolve-handler-use.ts +0 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -4
- package/src/route-types.ts +0 -5
- package/src/router/find-match.ts +86 -8
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +72 -12
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/manifest.ts +56 -11
- package/src/router/match-api.ts +76 -32
- package/src/router/match-handlers.ts +181 -135
- package/src/router/match-middleware/background-revalidation.ts +40 -23
- package/src/router/match-middleware/cache-store.ts +39 -24
- package/src/router/match-result.ts +35 -15
- package/src/router/middleware.ts +64 -38
- package/src/router/navigation-snapshot.ts +7 -5
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +53 -64
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +11 -5
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +23 -8
- package/src/router/route-snapshot.ts +14 -2
- package/src/router/router-context.ts +3 -1
- package/src/router/router-interfaces.ts +32 -1
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +39 -3
- package/src/router/segment-resolution/loader-cache.ts +93 -2
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +3 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router/trie-matching.ts +19 -11
- package/src/router/url-params.ts +13 -0
- package/src/router.ts +47 -16
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler.ts +60 -33
- package/src/rsc/manifest-init.ts +1 -1
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/progressive-enhancement.ts +61 -4
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +429 -37
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +30 -0
- package/src/segment-loader-promise.ts +18 -0
- package/src/segment-system.tsx +149 -14
- package/src/server/context.ts +67 -9
- package/src/server/cookie-store.ts +73 -1
- package/src/server/loader-registry.ts +13 -1
- package/src/server/request-context.ts +169 -10
- package/src/ssr/index.tsx +462 -178
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/testing/collect-handle.ts +14 -8
- package/src/testing/dispatch.ts +152 -40
- package/src/testing/generated-routes.ts +27 -11
- package/src/testing/index.ts +6 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/render-route.tsx +13 -10
- package/src/testing/run-transition-when.ts +164 -0
- package/src/theme/ThemeProvider.tsx +36 -26
- package/src/types/handler-context.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/route-config.ts +19 -7
- package/src/types/segments.ts +100 -0
- package/src/urls/include-helper.ts +10 -8
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +44 -12
- package/src/urls/path-helper.ts +5 -0
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +43 -18
- package/src/urls/urls-function.ts +0 -1
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +1 -1
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/index.ts +7 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugins/vercel-output.ts +114 -25
- package/src/vite/plugins/version-injector.ts +22 -7
- package/src/vite/plugins/virtual-entries.ts +80 -22
- package/src/vite/rango.ts +29 -19
- package/src/vite/router-discovery.ts +171 -43
- package/src/vite/utils/prerender-utils.ts +17 -4
- package/src/vite/utils/shared-utils.ts +47 -0
- package/src/network-error-thrower.tsx +0 -18
|
@@ -1,926 +1,52 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: typesafety
|
|
3
|
-
description: Set up type-safe routes, params, and environment types in @rangojs/router
|
|
3
|
+
description: Set up type-safe routes, params, and environment types in @rangojs/router. Use when route or search params aren't typed, TypeScript can't infer a loader's return type, or wiring up typed environment bindings.
|
|
4
4
|
argument-hint: [setup]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Type Safety Setup
|
|
8
8
|
|
|
9
|
-
@rangojs/router provides end-to-end type safety for routes, parameters, and
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- `
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
| `RegisteredRoutes` | manual `extends typeof router.routeMap` | global | paths, params, **response payloads** | the `Handler`/`Prerender` default (those read `GeneratedRouteMap` to avoid a `router.tsx` cycle) |
|
|
54
|
-
|
|
55
|
-
Key consequence: `href()` and the ambient `Rango.Path` type are typed from
|
|
56
|
-
whichever map is present — they prefer `RegisteredRoutes` when you wire it, otherwise fall back to
|
|
57
|
-
the auto-generated `GeneratedRouteMap`, so **`rango generate` alone gives you
|
|
58
|
-
path-checked `href()`** with no manual augmentation. Response and MIME payload
|
|
59
|
-
inference is the exception: it comes only from `typeof router.routeMap` (via
|
|
60
|
-
`RegisteredRoutes`), because `GeneratedRouteMap` carries paths + search but no
|
|
61
|
-
payloads — so `Rango.PathResponse` resolves to `never` until you wire
|
|
62
|
-
`RegisteredRoutes`.
|
|
63
|
-
|
|
64
|
-
Recommended setup:
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
// router.tsx
|
|
68
|
-
import { createRouter } from "@rangojs/router";
|
|
69
|
-
import { urlpatterns } from "./urls";
|
|
70
|
-
import type { AppBindings, AppVars } from "./env";
|
|
71
|
-
|
|
72
|
-
export const router = createRouter<AppBindings>({}).routes(urlpatterns);
|
|
73
|
-
|
|
74
|
-
declare global {
|
|
75
|
-
namespace Rango {
|
|
76
|
-
interface Env extends AppBindings {}
|
|
77
|
-
interface Vars extends AppVars {}
|
|
78
|
-
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Single-App Setup Checklist
|
|
84
|
-
|
|
85
|
-
For one app, keep the ambient types, generated named-routes file, and router
|
|
86
|
-
instance in the same TypeScript program:
|
|
87
|
-
|
|
88
|
-
```jsonc
|
|
89
|
-
// tsconfig.json
|
|
90
|
-
{
|
|
91
|
-
"compilerOptions": {
|
|
92
|
-
"strict": true,
|
|
93
|
-
"moduleResolution": "bundler",
|
|
94
|
-
"jsx": "react-jsx",
|
|
95
|
-
"noEmit": true,
|
|
96
|
-
},
|
|
97
|
-
"include": ["src"],
|
|
98
|
-
"files": ["src/router.tsx"],
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Then generate the route types from the router file:
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
npx rango generate src/router.tsx
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
This creates `src/router.named-routes.gen.ts`, which augments
|
|
109
|
-
`Rango.GeneratedRouteMap`. Keep that generated file committed with the router
|
|
110
|
-
source. The `files` entry keeps `router.tsx` in the program even when nothing
|
|
111
|
-
imports it directly, so `Rango.Env`, `Rango.Vars`, and optional
|
|
112
|
-
`Rango.RegisteredRoutes` augmentation are visible to handlers, loaders, actions,
|
|
113
|
-
and client helpers.
|
|
114
|
-
|
|
115
|
-
### Named Routes, `$$routeNames`, And `router.routeMap`
|
|
116
|
-
|
|
117
|
-
There are two runtime/type surfaces with similar names:
|
|
118
|
-
|
|
119
|
-
- `router.named-routes.gen.ts` exports `NamedRoutes` and augments
|
|
120
|
-
`Rango.GeneratedRouteMap`. The Vite plugin imports that file internally and
|
|
121
|
-
injects it as `$$routeNames` so `router.reverse` has the static route-name map.
|
|
122
|
-
App code should not pass or import `$$routeNames` directly.
|
|
123
|
-
- `router.routeMap` is the public router instance property for type extraction.
|
|
124
|
-
Use `typeof router.routeMap` when augmenting `Rango.RegisteredRoutes` for
|
|
125
|
-
global response payload helpers such as `Rango.PathResponse`.
|
|
126
|
-
|
|
127
|
-
Do not document or use a public `router.routeNames` API unless one is
|
|
128
|
-
intentionally added. Today, the public extraction surface is `router.routeMap`;
|
|
129
|
-
the generated file and `$$routeNames` are build machinery.
|
|
130
|
-
|
|
131
|
-
## Route Definition with Type-Safe Names
|
|
132
|
-
|
|
133
|
-
```typescript
|
|
134
|
-
// urls.tsx
|
|
135
|
-
import { urls } from "@rangojs/router";
|
|
136
|
-
|
|
137
|
-
export const urlpatterns = urls(({ path, layout }) => [
|
|
138
|
-
path("/", HomePage, { name: "home" }),
|
|
139
|
-
path("/products", ProductsPage, { name: "products" }),
|
|
140
|
-
path("/product/:slug", ProductPage, { name: "product" }),
|
|
141
|
-
path("/cart", CartPage, { name: "cart" }),
|
|
142
|
-
path("/checkout/:step?", CheckoutPage, { name: "checkout" }),
|
|
143
|
-
]);
|
|
144
|
-
|
|
145
|
-
// Route names are inferred from the { name } option
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## Type-Safe href()
|
|
149
|
-
|
|
150
|
-
### Server: ctx.reverse with route names
|
|
151
|
-
|
|
152
|
-
In route handlers, `ctx.reverse()` uses two namespaces:
|
|
153
|
-
|
|
154
|
-
- **`.name`** — local route, resolved within the current `include()` scope
|
|
155
|
-
- **`name`** — global route, from the named-routes definition
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
import type { Handler } from "@rangojs/router";
|
|
159
|
-
|
|
160
|
-
export const ProductHandler: Handler<"shop.product"> = (ctx) => {
|
|
161
|
-
ctx.reverse(".cart"); // Local: /shop/cart
|
|
162
|
-
ctx.reverse(".product", { slug: "widget" }); // Local: /shop/product/widget
|
|
163
|
-
ctx.reverse("blog.post", { slug: "1" }); // Global: /blog/1
|
|
164
|
-
};
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
For type-safe local names, generate a route types file with `npx rango generate urls/shop.tsx`
|
|
168
|
-
and pass it as the second generic to `Handler` or `Prerender`:
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
import type { Handler } from "@rangojs/router";
|
|
172
|
-
import type { routes } from "./shop.gen.js";
|
|
173
|
-
|
|
174
|
-
export const ProductHandler: Handler<"shop.product", routes> = (ctx) => {
|
|
175
|
-
ctx.reverse(".cart"); // Type-safe local name
|
|
176
|
-
ctx.reverse(".product", { slug: "widget" }); // Type-safe local with params
|
|
177
|
-
ctx.reverse("blog.post", { slug: "hi" }); // Type-safe global name
|
|
178
|
-
};
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### Client: href + useHref
|
|
182
|
-
|
|
183
|
-
On the client, `href()` validates paths against registered route patterns at compile time:
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
"use client";
|
|
187
|
-
import { href, useHref, Link } from "@rangojs/router/client";
|
|
188
|
-
|
|
189
|
-
// href() validates absolute paths via PatternToPath types
|
|
190
|
-
href("/about"); // Valid path
|
|
191
|
-
href("/blog/hello"); // Matches /blog/:slug
|
|
192
|
-
|
|
193
|
-
// useHref() auto-prefixes with include() mount
|
|
194
|
-
function ShopNav() {
|
|
195
|
-
const href = useHref();
|
|
196
|
-
return <Link to={href("/cart")}>Cart</Link>; // "/shop/cart"
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
`href()` and the `Rango.Path` type read from `RegisteredRoutes` when you augment
|
|
201
|
-
it, otherwise from the auto-generated `GeneratedRouteMap` — so `rango generate`
|
|
202
|
-
alone type-checks `href()` paths with no manual augmentation. The augmentation
|
|
203
|
-
below is only needed for **`Rango.PathResponse`** (response-payload inference), which
|
|
204
|
-
`GeneratedRouteMap` cannot provide:
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
207
|
-
declare global {
|
|
208
|
-
namespace Rango {
|
|
209
|
-
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
For wrapper helpers, type the path parameter as `Rango.Path`. It is ambient (no
|
|
215
|
-
import) and shares `href()`'s compile-time path checking, so a wrapper stays in
|
|
216
|
-
sync with your routes automatically:
|
|
217
|
-
|
|
218
|
-
```typescript
|
|
219
|
-
import { href } from "@rangojs/router/client";
|
|
220
|
-
|
|
221
|
-
export const appHref = (path: Rango.Path): string => href(path);
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
For response-route payloads, `Rango.PathResponse<T>` is the ambient lookup. It
|
|
225
|
-
accepts a route _pattern_ **or** a concrete path, so it also serves as the return
|
|
226
|
-
type of a typed `fetch` wrapper. It only resolves once `RegisteredRoutes` carries
|
|
227
|
-
response metadata:
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
import { href } from "@rangojs/router/client";
|
|
231
|
-
|
|
232
|
-
type Product = Rango.PathResponse<"/api/products/:id">; // by pattern
|
|
233
|
-
type Same = Rango.PathResponse<"/api/products/42">; // by concrete path
|
|
234
|
-
|
|
235
|
-
// Response inferred from the concrete path passed in:
|
|
236
|
-
async function get<T extends Rango.Path>(
|
|
237
|
-
path: T,
|
|
238
|
-
): Promise<Rango.PathResponse<T>> {
|
|
239
|
-
return fetch(href(path)).then((r) => r.json());
|
|
240
|
-
}
|
|
241
|
-
const product = await get("/api/products/42"); // Product (bare value)
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
Pattern keys (`/:id`) match exactly; a concrete path under a _nested_ dynamic
|
|
245
|
-
route can match several patterns and union their responses.
|
|
246
|
-
|
|
247
|
-
`Rango.PathResponse` describes the JSON **wire** shape, not the handler's raw
|
|
248
|
-
return. A `path.json()` handler returning `{ createdAt: Date }` resolves here to
|
|
249
|
-
`{ createdAt: string }` (bare value), matching what `r.json()` yields. This
|
|
250
|
-
is applied via the ambient `Rango.JsonSerialize<T>` transform (`Date -> string`,
|
|
251
|
-
honors `toJSON()`, drops functions/`undefined`, `bigint -> never`). A separate
|
|
252
|
-
`Rango.FlightSerialize<T>` models the higher-fidelity RSC Flight boundary
|
|
253
|
-
(loaders / RSC props, where `Date` is preserved) — do **not** use it for
|
|
254
|
-
`path.json()`.
|
|
255
|
-
|
|
256
|
-
### Overriding serialization globally
|
|
257
|
-
|
|
258
|
-
For your own types, the zero-config way to control the JSON wire shape is a
|
|
259
|
-
`toJSON()` method — `Rango.JsonSerialize` honors it, and it matches the runtime
|
|
260
|
-
exactly (`JSON.stringify` calls `toJSON()`):
|
|
261
|
-
|
|
262
|
-
```typescript
|
|
263
|
-
class Money {
|
|
264
|
-
constructor(private cents: number) {}
|
|
265
|
-
toJSON(): number {
|
|
266
|
-
return this.cents;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
// Rango.JsonSerialize<Money> is number; Rango.PathResponse reflects it.
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
To override a transform for types you **don't** own (or for the Flight boundary,
|
|
273
|
-
which has no `toJSON()`), augment its override slot. Because `Rango.JsonSerialize`
|
|
274
|
-
/ `Rango.FlightSerialize` are type _aliases_ (TS can't merge those), you provide a
|
|
275
|
-
single member that is your **complete** transform, delegating to the built-in for
|
|
276
|
-
the cases you don't change:
|
|
277
|
-
|
|
278
|
-
```typescript
|
|
279
|
-
declare global {
|
|
280
|
-
namespace Rango {
|
|
281
|
-
interface JsonSerializeOverride<T> {
|
|
282
|
-
app: T extends Decimal ? string : Rango.JsonSerializeBuiltin<T>;
|
|
283
|
-
}
|
|
284
|
-
interface FlightSerializeOverride<T> {
|
|
285
|
-
app: T extends Money ? number : Rango.FlightSerializeBuiltin<T>;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
// Rango.JsonSerialize<Decimal> -> string; Rango.FlightSerialize<Money> -> number;
|
|
290
|
-
// everything else stays on the built-in, recursively (nested fields too).
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
Rules: provide **exactly one** member (the slot is read as
|
|
294
|
-
`Override<T>[keyof Override<T>]`, so multiple members union and conflict).
|
|
295
|
-
Overrides win over `toJSON()` and apply at every nesting level. Caveat for JSON:
|
|
296
|
-
the `path.json()` runtime is plain `JSON.stringify`, which only honors `toJSON()`,
|
|
297
|
-
so a `JsonSerializeOverride` that disagrees with what the runtime emits will lie —
|
|
298
|
-
prefer `toJSON()` for your own types and use the slot only for types you can't
|
|
299
|
-
modify.
|
|
300
|
-
|
|
301
|
-
See `/links` for full URL generation guide.
|
|
302
|
-
|
|
303
|
-
## Environment Type Setup
|
|
304
|
-
|
|
305
|
-
Define your app's environment for type-safe bindings and variables:
|
|
306
|
-
|
|
307
|
-
```typescript
|
|
308
|
-
// env.ts
|
|
309
|
-
|
|
310
|
-
// Cloudflare bindings — passed as TEnv to createRouter<TEnv>()
|
|
311
|
-
export interface AppBindings {
|
|
312
|
-
DB: D1Database;
|
|
313
|
-
KV: KVNamespace;
|
|
314
|
-
CACHE: KVNamespace;
|
|
315
|
-
AI: Ai;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Variables set by middleware — declared via global namespace augmentation
|
|
319
|
-
export interface AppVariables {
|
|
320
|
-
user?: { id: string; email: string; role: string };
|
|
321
|
-
requestId?: string;
|
|
322
|
-
permissions?: string[];
|
|
323
|
-
}
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
### Using Environment Types
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
// router.tsx
|
|
330
|
-
import type { AppBindings, AppVariables } from "./env";
|
|
331
|
-
|
|
332
|
-
const router = createRouter<AppBindings>({
|
|
333
|
-
document: Document,
|
|
334
|
-
}).routes(urlpatterns);
|
|
335
|
-
|
|
336
|
-
// Register bindings and variables globally for implicit typing
|
|
337
|
-
declare global {
|
|
338
|
-
namespace Rango {
|
|
339
|
-
interface Env extends AppBindings {}
|
|
340
|
-
interface Vars extends AppVariables {}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// middleware - typed via ctx.set / ctx.get
|
|
345
|
-
import type { Middleware } from "@rangojs/router";
|
|
346
|
-
|
|
347
|
-
export const authMiddleware: Middleware = async (ctx, next) => {
|
|
348
|
-
ctx.set("user", {
|
|
349
|
-
id: "123",
|
|
350
|
-
email: "user@example.com",
|
|
351
|
-
role: "admin",
|
|
352
|
-
});
|
|
353
|
-
await next();
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
// loaders - typed context
|
|
357
|
-
export const UserLoader = createLoader(async (ctx) => {
|
|
358
|
-
const db = ctx.env.DB; // D1Database (plain bindings)
|
|
359
|
-
const userId = ctx.get("user")?.id; // from Rango.Vars
|
|
360
|
-
return db.prepare("SELECT * FROM users WHERE id = ?").bind(userId).first();
|
|
361
|
-
});
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
## Global Environment Registration
|
|
365
|
-
|
|
366
|
-
Register environment types globally for implicit typing:
|
|
367
|
-
|
|
368
|
-
```typescript
|
|
369
|
-
// router.tsx
|
|
370
|
-
declare global {
|
|
371
|
-
namespace Rango {
|
|
372
|
-
interface Env extends AppBindings {}
|
|
373
|
-
interface Vars extends AppVariables {}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
Now handlers have typed context without explicit imports:
|
|
379
|
-
|
|
380
|
-
```typescript
|
|
381
|
-
// In loaders
|
|
382
|
-
export const DashboardLoader = createLoader(async (ctx) => {
|
|
383
|
-
// ctx.env.DB is typed from global Rango.Env
|
|
384
|
-
// ctx.get("user") is typed from global Rango.Vars
|
|
385
|
-
const user = ctx.get("user");
|
|
386
|
-
return { user };
|
|
387
|
-
});
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
## Typed Search Params
|
|
391
|
-
|
|
392
|
-
Add a `search` schema to `path()` options for type-safe query parameters:
|
|
393
|
-
|
|
394
|
-
```typescript
|
|
395
|
-
// Route definition with search schema
|
|
396
|
-
path("/search", SearchPage, {
|
|
397
|
-
name: "search",
|
|
398
|
-
search: { q: "string", page: "number?", sort: "string?" },
|
|
399
|
-
});
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
### Handler with typed search params
|
|
403
|
-
|
|
404
|
-
`Handler<"name">` automatically resolves route params and search params from the
|
|
405
|
-
global `GeneratedRouteMap` (the gen file). No explicit route map import needed:
|
|
406
|
-
|
|
407
|
-
```typescript
|
|
408
|
-
// pages/search.tsx
|
|
409
|
-
import type { Handler } from "@rangojs/router";
|
|
410
|
-
|
|
411
|
-
export const SearchPage: Handler<"search"> = (ctx) => {
|
|
412
|
-
// ctx.search is typed: { q: string; page?: number; sort?: string }
|
|
413
|
-
const { q, page, sort } = ctx.search;
|
|
414
|
-
return <SearchResults q={q} page={page} sort={sort} />;
|
|
415
|
-
};
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
This avoids circular references because `Handler` defaults to `GeneratedRouteMap`
|
|
419
|
-
(from `router.named-routes.gen.ts`) instead of `RegisteredRoutes` (which depends on `router.tsx`).
|
|
420
|
-
|
|
421
|
-
You can also pass an explicit route map for per-module isolation (opt-in,
|
|
422
|
-
after running `npx rango generate`). With a local map, the route name is
|
|
423
|
-
**dot-prefixed** so params and search resolve from `routes`, not the global map:
|
|
424
|
-
|
|
425
|
-
```typescript
|
|
426
|
-
import type { Handler } from "@rangojs/router";
|
|
427
|
-
import type { routes } from "./urls.gen.js";
|
|
428
|
-
|
|
429
|
-
export const SearchPage: Handler<".search", routes> = (ctx) => { ... };
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
Note the difference: `Handler<"search">` (no dot) resolves against the global
|
|
433
|
-
`GeneratedRouteMap`; `Handler<".search", routes>` resolves against the local
|
|
434
|
-
`routes` map. Mixing them — `Handler<"search", routes>` — silently ignores
|
|
435
|
-
`routes` for param/search inference and only uses it for local `ctx.reverse(".x")`.
|
|
436
|
-
|
|
437
|
-
Supported types: `"string"`, `"number"`, `"boolean"`, with `?` suffix for optional.
|
|
438
|
-
Values are automatically coerced from query string (e.g., `"2"` becomes `2` for numbers).
|
|
439
|
-
Routes without a `search` schema keep the standard `URLSearchParams` behavior.
|
|
440
|
-
|
|
441
|
-
### RouteSearchParams and RouteParams utility types
|
|
442
|
-
|
|
443
|
-
Extract typed params by route name for use in component props, return types, or anywhere:
|
|
444
|
-
|
|
445
|
-
```typescript
|
|
446
|
-
import type { RouteSearchParams, RouteParams } from "@rangojs/router";
|
|
447
|
-
|
|
448
|
-
// RouteSearchParams<"name"> resolves the search schema to a typed object
|
|
449
|
-
type SP = RouteSearchParams<"search">;
|
|
450
|
-
// { q: string | undefined; page?: number; sort?: string }
|
|
451
|
-
|
|
452
|
-
// RouteParams<"name"> resolves URL params from the route pattern
|
|
453
|
-
type P = RouteParams<"blogPost">;
|
|
454
|
-
// { slug: string }
|
|
455
|
-
|
|
456
|
-
// Optional URL params (`:slug?`) resolve to `string | undefined`
|
|
457
|
-
// because absent segments are omitted from `ctx.params` at runtime.
|
|
458
|
-
type C = RouteParams<"checkout">;
|
|
459
|
-
// { step?: string }
|
|
460
|
-
// → ctx.params.step is `string | undefined`; use `?? "default"` to coalesce.
|
|
461
|
-
|
|
462
|
-
// Use in component props
|
|
463
|
-
interface SearchResultsProps {
|
|
464
|
-
params: RouteSearchParams<"search">;
|
|
465
|
-
}
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
Both default to the global route map (`RegisteredRoutes` or `GeneratedRouteMap`).
|
|
469
|
-
Pass an explicit route map as the second type argument when needed:
|
|
470
|
-
|
|
471
|
-
```typescript
|
|
472
|
-
import type { routes } from "./urls.gen.js";
|
|
473
|
-
|
|
474
|
-
type SP = RouteSearchParams<"search", routes>;
|
|
475
|
-
type P = RouteParams<"blogPost", routes>;
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
### Generated route types
|
|
479
|
-
|
|
480
|
-
In the generated `router.named-routes.gen.ts`, routes with search schemas
|
|
481
|
-
use `{ path, search }` objects:
|
|
482
|
-
|
|
483
|
-
```typescript
|
|
484
|
-
// router.named-routes.gen.ts (auto-generated)
|
|
485
|
-
export const NamedRoutes = {
|
|
486
|
-
"search.index": {
|
|
487
|
-
path: "/search",
|
|
488
|
-
search: { q: "string", page: "number?", sort: "string?" },
|
|
489
|
-
},
|
|
490
|
-
"home.index": "/", // No search schema -> plain string
|
|
491
|
-
} as const;
|
|
492
|
-
```
|
|
493
|
-
|
|
494
|
-
You never open a `.gen.ts` by hand. Treat the generated types as call-site
|
|
495
|
-
honesty checks, not modules to read:
|
|
496
|
-
|
|
497
|
-
- **Do not import `router.named-routes.gen.ts` directly**, and don't reach for
|
|
498
|
-
`Rango.GeneratedRouteMap`. It is the whole-app manifest, auto-wired
|
|
499
|
-
globally — `Handler<"name">` and `ctx.reverse("name")` already see it.
|
|
500
|
-
- **Per-module `*.gen.ts` imports are fine** — they are the opt-in local-route
|
|
501
|
-
pattern for `useReverse(routes)` and explicit local handler typing
|
|
502
|
-
(`Handler<".name", routes>`). See `/links`.
|
|
503
|
-
|
|
504
|
-
If a type error points at a generated map instead of your call site, that's a
|
|
505
|
-
smell — fix the call site (or regenerate), never edit the generated file.
|
|
506
|
-
|
|
507
|
-
## Loader Type Safety
|
|
508
|
-
|
|
509
|
-
Loaders have typed return values:
|
|
510
|
-
|
|
511
|
-
```typescript
|
|
512
|
-
// loaders/product.ts
|
|
513
|
-
export const ProductLoader = createLoader(async (ctx) => {
|
|
514
|
-
return {
|
|
515
|
-
id: ctx.params.slug,
|
|
516
|
-
name: "Widget",
|
|
517
|
-
price: 99,
|
|
518
|
-
};
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
// In server component - type is inferred
|
|
522
|
-
import { useLoader } from "@rangojs/router/client";
|
|
523
|
-
|
|
524
|
-
async function ProductPage() {
|
|
525
|
-
const product = await useLoader(ProductLoader);
|
|
526
|
-
// product: { id: string; name: string; price: number }
|
|
527
|
-
return <h1>{product.name}</h1>;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// In client component - same type
|
|
531
|
-
"use client";
|
|
532
|
-
import { useLoader } from "@rangojs/router/client";
|
|
533
|
-
|
|
534
|
-
function ProductPrice() {
|
|
535
|
-
const { data } = useLoader(ProductLoader);
|
|
536
|
-
// data: { id: string; name: string; price: number }
|
|
537
|
-
const product = data;
|
|
538
|
-
return <span>${product.price}</span>;
|
|
539
|
-
}
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
## Typed Context Variables
|
|
543
|
-
|
|
544
|
-
`createVar<T>()` creates a typed token for `ctx.set()`/`ctx.get()`, making
|
|
545
|
-
handler-to-layout data contracts explicit and compile-time verified:
|
|
546
|
-
|
|
547
|
-
```typescript
|
|
548
|
-
import { createVar } from "@rangojs/router";
|
|
549
|
-
|
|
550
|
-
// Define a typed token (shared between producer and consumer)
|
|
551
|
-
interface PaginationData {
|
|
552
|
-
current: number;
|
|
553
|
-
total: number;
|
|
554
|
-
perPage: number;
|
|
555
|
-
}
|
|
556
|
-
export const Pagination = createVar<PaginationData>();
|
|
557
|
-
|
|
558
|
-
// Non-cacheable var — reading inside cache() or "use cache" throws at runtime
|
|
559
|
-
const Session = createVar<SessionData>({ cache: false });
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
`createVar` accepts an optional options object. The `cache` option (default
|
|
563
|
-
`true`) controls whether the var's values can be read inside cache scopes.
|
|
564
|
-
Write-level escalation is also supported: `ctx.set(Var, value, { cache: false })`
|
|
565
|
-
marks a specific write as non-cacheable even if the var itself is cacheable.
|
|
566
|
-
"Least cacheable wins" — if either says `cache: false`, the value throws on
|
|
567
|
-
read inside `cache()` or `"use cache"`.
|
|
568
|
-
|
|
569
|
-
### Producer (handler or middleware)
|
|
570
|
-
|
|
571
|
-
```typescript
|
|
572
|
-
import { Pagination } from "../vars/pagination.js";
|
|
573
|
-
|
|
574
|
-
const ArticleList: Handler<"articles.list"> = async (ctx) => {
|
|
575
|
-
ctx.set(Pagination, { // type-checked
|
|
576
|
-
current: 1,
|
|
577
|
-
total: 10,
|
|
578
|
-
perPage: 5,
|
|
579
|
-
});
|
|
580
|
-
return <Articles />;
|
|
581
|
-
};
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
### Consumer (layout, parallel, or any context with get)
|
|
585
|
-
|
|
586
|
-
```typescript
|
|
587
|
-
import { Pagination } from "../vars/pagination.js";
|
|
588
|
-
|
|
589
|
-
export function PaginationLayout(ctx: any) {
|
|
590
|
-
const pagination = ctx.get(Pagination); // typed as PaginationData | undefined
|
|
591
|
-
if (!pagination) return <Outlet />;
|
|
592
|
-
return <nav>Page {pagination.current} of {pagination.total}</nav>;
|
|
593
|
-
}
|
|
594
|
-
```
|
|
595
|
-
|
|
596
|
-
### Why not just use Rango.Vars?
|
|
597
|
-
|
|
598
|
-
`Rango.Vars` (via global namespace augmentation) provides app-global typing for
|
|
599
|
-
`ctx.get("key")` / `ctx.set("key", value)`. It works for middleware state
|
|
600
|
-
shared app-wide. `createVar<T>()` is for route-local or feature-scoped
|
|
601
|
-
context -- the producer and consumer import the same token, creating a
|
|
602
|
-
scoped contract without polluting global types.
|
|
603
|
-
|
|
604
|
-
Both approaches coexist: `ctx.get("user")` (global via Vars) and
|
|
605
|
-
`ctx.get(Pagination)` (scoped via createVar) work side by side.
|
|
606
|
-
|
|
607
|
-
## Handle Type Safety
|
|
608
|
-
|
|
609
|
-
Handles have typed data:
|
|
610
|
-
|
|
611
|
-
```typescript
|
|
612
|
-
// Built-in Breadcrumbs handle — import from "@rangojs/router"
|
|
613
|
-
import { Breadcrumbs } from "@rangojs/router";
|
|
614
|
-
// Type: Handle<BreadcrumbItem, BreadcrumbItem[]>
|
|
615
|
-
// BreadcrumbItem: { label: string; href: string; content?: ReactNode | Promise<ReactNode> }
|
|
616
|
-
|
|
617
|
-
// In route handler — push is fully typed
|
|
618
|
-
path("/shop/product/:slug", (ctx) => {
|
|
619
|
-
const breadcrumb = ctx.use(Breadcrumbs);
|
|
620
|
-
breadcrumb({ label: "Products", href: "/shop/products" });
|
|
621
|
-
return <ProductPage />;
|
|
622
|
-
}, { name: "product" });
|
|
623
|
-
|
|
624
|
-
// In client — typed array
|
|
625
|
-
import { useHandle, Breadcrumbs } from "@rangojs/router/client";
|
|
626
|
-
function BreadcrumbNav() {
|
|
627
|
-
const crumbs = useHandle(Breadcrumbs);
|
|
628
|
-
// crumbs: BreadcrumbItem[]
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// Custom handles also work the same way
|
|
632
|
-
import { createHandle } from "@rangojs/router";
|
|
633
|
-
export const PageTitle = createHandle<string, string>(
|
|
634
|
-
(segments) => segments.flat().at(-1) ?? "Default Title"
|
|
635
|
-
);
|
|
636
|
-
```
|
|
637
|
-
|
|
638
|
-
## Ref Prop Type Safety (Loaders & Handles)
|
|
639
|
-
|
|
640
|
-
Loaders and handles can be passed as props from server to client components.
|
|
641
|
-
Use `typeof` to get the full typed definition without manually specifying generics:
|
|
642
|
-
|
|
643
|
-
```typescript
|
|
644
|
-
// loaders.ts
|
|
645
|
-
export const ProductLoader = createLoader(async (ctx) => {
|
|
646
|
-
return { product: await fetchProduct(ctx.params.slug) };
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
// Built-in Breadcrumbs — or any custom handle created with createHandle()
|
|
650
|
-
```
|
|
651
|
-
|
|
652
|
-
```tsx
|
|
653
|
-
// Client component — typeof infers all generics
|
|
654
|
-
"use client";
|
|
655
|
-
import { useLoader, useHandle, type Breadcrumbs } from "@rangojs/router/client";
|
|
656
|
-
import type { ProductLoader } from "../loaders";
|
|
657
|
-
|
|
658
|
-
function MyComponent({
|
|
659
|
-
loader,
|
|
660
|
-
handle,
|
|
661
|
-
}: {
|
|
662
|
-
loader: typeof ProductLoader; // LoaderDefinition<{ product: Product }>
|
|
663
|
-
handle: typeof Breadcrumbs; // Handle<{ label: string; href: string }>
|
|
664
|
-
}) {
|
|
665
|
-
const { data } = useLoader(loader); // data is typed
|
|
666
|
-
const crumbs = useHandle(handle); // crumbs is typed array
|
|
667
|
-
// ...
|
|
668
|
-
}
|
|
669
|
-
```
|
|
670
|
-
|
|
671
|
-
RSC Flight serialization calls `toJSON()` on both loaders and handles,
|
|
672
|
-
sending only `{ __brand, $$id }` to the client. The hooks recover the
|
|
673
|
-
full functionality from module-level registries.
|
|
674
|
-
|
|
675
|
-
## Stable identity: `path#export`
|
|
676
|
-
|
|
677
|
-
Loaders, handles, cached functions (`functionId`), and server actions
|
|
678
|
-
(`actionId`) all share one identity scheme: `{modulePath}#{exportName}`,
|
|
679
|
-
injected at build by the `exposeInternalIds` and `exposeActionId` Vite plugins.
|
|
680
|
-
This is also the identity React server actions carry across the Flight boundary,
|
|
681
|
-
which is why a `revalidate()` predicate sees an action as a `path#export` string:
|
|
682
|
-
|
|
683
|
-
```typescript
|
|
684
|
-
revalidate(
|
|
685
|
-
({ actionId }) => actionId === "src/actions/cart.ts#addToCart" || undefined,
|
|
686
|
-
);
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
`actionId` is the only stable reference React exposes across the Flight boundary,
|
|
690
|
-
so it stays as the floor and escape hatch. The hand-written-string surface
|
|
691
|
-
(`actionId?.includes("cart.ts#")`) is brittle: a renamed action or moved file
|
|
692
|
-
silently stops matching with no compile error. Prefer **`ctx.isAction()`** in a
|
|
693
|
-
revalidate predicate — it resolves the action's id from an imported reference, so
|
|
694
|
-
a rename is a type error in one place instead of silent drift:
|
|
695
|
-
|
|
696
|
-
```ts
|
|
697
|
-
import { addToCart, removeFromCart } from "./actions/cart";
|
|
698
|
-
import * as CartActions from "./actions/cart";
|
|
699
|
-
|
|
700
|
-
revalidate((ctx) => ctx.isAction(addToCart) || undefined); // one action
|
|
701
|
-
revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
|
|
702
|
-
revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any action in the module
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
`ctx.isAction()` (only available on the revalidate predicate's context) returns a
|
|
706
|
-
raw boolean — combine with `|| undefined` for the "revalidate on match, else
|
|
707
|
-
defer" intent. It resolves the reference the same way the router derives
|
|
708
|
-
`actionId` (`$id` in production, `$$id` in dev), so matching
|
|
709
|
-
works in both modes. `actionId` stays available for advanced cases.
|
|
710
|
-
|
|
711
|
-
## Location State Type Safety
|
|
712
|
-
|
|
713
|
-
```typescript
|
|
714
|
-
// location-states.ts
|
|
715
|
-
import { createLocationState } from "@rangojs/router";
|
|
716
|
-
|
|
717
|
-
// All export patterns work: export const, const + export { X }, export { X as Y }
|
|
718
|
-
export const ProductPreview = createLocationState<{
|
|
719
|
-
name: string;
|
|
720
|
-
price: number;
|
|
721
|
-
image: string;
|
|
722
|
-
}>();
|
|
723
|
-
|
|
724
|
-
// Passing state through Link
|
|
725
|
-
<Link
|
|
726
|
-
to={href("product", { slug: "widget" })}
|
|
727
|
-
state={[ProductPreview({ name: "Widget", price: 99, image: "/img.jpg" })]}
|
|
728
|
-
>
|
|
729
|
-
View Product
|
|
730
|
-
</Link>
|
|
731
|
-
|
|
732
|
-
// Reading state in component
|
|
733
|
-
function ProductHeader() {
|
|
734
|
-
const preview = useLocationState(ProductPreview);
|
|
735
|
-
// preview: { name: string; price: number; image: string } | undefined
|
|
736
|
-
|
|
737
|
-
if (preview) {
|
|
738
|
-
return <h1>{preview.name} - ${preview.price}</h1>;
|
|
739
|
-
}
|
|
740
|
-
return <h1>Loading...</h1>;
|
|
741
|
-
}
|
|
742
|
-
```
|
|
743
|
-
|
|
744
|
-
## Multi-Project tsconfig Setup
|
|
745
|
-
|
|
746
|
-
For monorepos or multi-app setups, each app should have its own TypeScript
|
|
747
|
-
program. Do not typecheck two Rango apps with different `Rango.Env`,
|
|
748
|
-
`Rango.Vars`, or `Rango.RegisteredRoutes` declarations in one tsconfig, because
|
|
749
|
-
ambient global interfaces merge across the whole program.
|
|
750
|
-
|
|
751
|
-
### Multiple routers in one program
|
|
752
|
-
|
|
753
|
-
`Rango.GeneratedRouteMap` is a **single global interface**. Each router's
|
|
754
|
-
generated `router.named-routes.gen.ts` augments it, so two routers in the **same
|
|
755
|
-
TS program** that define overlapping route names (e.g. both have a `home`) make
|
|
756
|
-
the augmentations collide:
|
|
757
|
-
|
|
758
|
-
```text
|
|
759
|
-
Interface 'GeneratedRouteMap' cannot simultaneously extend ...
|
|
760
|
-
Named property 'home' ... are not identical.
|
|
761
|
-
```
|
|
762
|
-
|
|
763
|
-
This is the multi-router / host-router case. Resolve it by:
|
|
764
|
-
|
|
765
|
-
- **Separate TS programs** — give each router its own tsconfig (as below) so only
|
|
766
|
-
one generated map is in scope per program. Recommended.
|
|
767
|
-
- **Unique route-name prefixes** — name routes per router (`appA.home`,
|
|
768
|
-
`appB.home`) so the merged global map has no duplicate keys.
|
|
769
|
-
|
|
770
|
-
A single global generated map is a single-router convenience; global named-route
|
|
771
|
-
typing across multiple routers in one program is not supported today (it would
|
|
772
|
-
need per-router scoping in the generated map).
|
|
773
|
-
|
|
774
|
-
Use a shared base tsconfig for common compiler options, then make every app
|
|
775
|
-
tsconfig include its own source tree, its own `router.tsx`, and the generated
|
|
776
|
-
`router.named-routes.gen.ts` that lives beside that router.
|
|
777
|
-
|
|
778
|
-
```jsonc
|
|
779
|
-
// tsconfig.base.json (root)
|
|
780
|
-
{
|
|
781
|
-
"compilerOptions": {
|
|
782
|
-
"target": "ES2022",
|
|
783
|
-
"module": "ESNext",
|
|
784
|
-
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
785
|
-
"jsx": "react-jsx",
|
|
786
|
-
"moduleResolution": "bundler",
|
|
787
|
-
"strict": true,
|
|
788
|
-
"noEmit": true,
|
|
789
|
-
"skipLibCheck": true,
|
|
790
|
-
"isolatedModules": true,
|
|
791
|
-
"esModuleInterop": true,
|
|
792
|
-
"resolveJsonModule": true,
|
|
793
|
-
},
|
|
794
|
-
}
|
|
795
|
-
```
|
|
796
|
-
|
|
797
|
-
```jsonc
|
|
798
|
-
// apps/shop/tsconfig.json
|
|
799
|
-
{
|
|
800
|
-
"extends": "../../tsconfig.base.json",
|
|
801
|
-
"include": ["src"],
|
|
802
|
-
"files": ["src/router.tsx"],
|
|
803
|
-
}
|
|
804
|
-
```
|
|
805
|
-
|
|
806
|
-
```jsonc
|
|
807
|
-
// apps/blog/tsconfig.json
|
|
808
|
-
{
|
|
809
|
-
"extends": "../../tsconfig.base.json",
|
|
810
|
-
"include": ["src"],
|
|
811
|
-
"files": ["src/router.tsx"],
|
|
812
|
-
}
|
|
813
|
-
```
|
|
814
|
-
|
|
815
|
-
Run generation per app:
|
|
816
|
-
|
|
817
|
-
```bash
|
|
818
|
-
npx rango generate apps/shop/src/router.tsx
|
|
819
|
-
npx rango generate apps/blog/src/router.tsx
|
|
820
|
-
```
|
|
821
|
-
|
|
822
|
-
If an app has multiple tsconfigs (`tsconfig.app.json`, `tsconfig.test.json`,
|
|
823
|
-
`tsconfig.worker.json`), every tsconfig that typechecks Rango handlers,
|
|
824
|
-
components, loaders, actions, or client navigation must see the same app-local
|
|
825
|
-
type surfaces:
|
|
826
|
-
|
|
827
|
-
```jsonc
|
|
828
|
-
// apps/shop/tsconfig.test.json
|
|
829
|
-
{
|
|
830
|
-
"extends": "./tsconfig.json",
|
|
831
|
-
"include": ["src", "tests"],
|
|
832
|
-
"files": ["src/router.tsx"],
|
|
833
|
-
}
|
|
834
|
-
```
|
|
835
|
-
|
|
836
|
-
The `files` array ensures `router.tsx` is always included even if nothing
|
|
837
|
-
directly imports it. The generated `router.named-routes.gen.ts` is normally
|
|
838
|
-
covered by `include: ["src"]`; if a tsconfig uses a narrow `include`, add the
|
|
839
|
-
generated file explicitly. Each app gets its own typed environment and named
|
|
840
|
-
route map without interfering with other apps.
|
|
841
|
-
|
|
842
|
-
For response and MIME payload lookup in each app, augment `RegisteredRoutes`
|
|
843
|
-
inside that app's router file:
|
|
844
|
-
|
|
845
|
-
```typescript
|
|
846
|
-
// apps/shop/src/router.tsx
|
|
847
|
-
export const router = createRouter<ShopEnv>({ document: Document }).routes(
|
|
848
|
-
urlpatterns,
|
|
849
|
-
);
|
|
850
|
-
|
|
851
|
-
declare global {
|
|
852
|
-
namespace Rango {
|
|
853
|
-
interface Env extends ShopEnv {}
|
|
854
|
-
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
```
|
|
858
|
-
|
|
859
|
-
## Complete Type-Safe Setup
|
|
860
|
-
|
|
861
|
-
```typescript
|
|
862
|
-
// 1. env.ts - Environment types
|
|
863
|
-
export interface AppBindings {
|
|
864
|
-
DB: D1Database;
|
|
865
|
-
KV: KVNamespace;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
export interface AppVariables {
|
|
869
|
-
user?: { id: string; email: string; role: string };
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// 2. urls.tsx - Route definitions with names
|
|
873
|
-
import { urls } from "@rangojs/router";
|
|
874
|
-
|
|
875
|
-
export const urlpatterns = urls(({ path, layout, loader }) => [
|
|
876
|
-
path("/", HomePage, { name: "home" }),
|
|
877
|
-
|
|
878
|
-
layout(<ShopLayout />, () => [
|
|
879
|
-
path("/shop", ShopIndex, { name: "shop" }),
|
|
880
|
-
path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
|
|
881
|
-
loader(ProductLoader),
|
|
882
|
-
]),
|
|
883
|
-
]),
|
|
884
|
-
]);
|
|
885
|
-
|
|
886
|
-
// 3. router.tsx - Create router and export reverse
|
|
887
|
-
const router = createRouter<AppBindings>({
|
|
888
|
-
document: Document,
|
|
889
|
-
}).routes(urlpatterns);
|
|
890
|
-
|
|
891
|
-
// Register bindings and variables globally for implicit typing
|
|
892
|
-
declare global {
|
|
893
|
-
namespace Rango {
|
|
894
|
-
interface Env extends AppBindings {}
|
|
895
|
-
interface Vars extends AppVariables {}
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
export const reverse = router.reverse;
|
|
900
|
-
export default router;
|
|
901
|
-
|
|
902
|
-
// 4. Run `npx rango generate src/router.tsx` to generate
|
|
903
|
-
// router.named-routes.gen.ts (auto-registers GeneratedRouteMap globally).
|
|
904
|
-
// No manual RegisteredRoutes declaration is needed for named-route handlers,
|
|
905
|
-
// ctx.reverse, prerender, href(), or Rango.Path. Add `RegisteredRoutes
|
|
906
|
-
// extends typeof router.routeMap` when global response payload helpers such
|
|
907
|
-
// as Rango.PathResponse need the richer router.routeMap metadata.
|
|
908
|
-
|
|
909
|
-
// 5. loaders/*.ts - Type-safe loaders
|
|
910
|
-
export const ProductLoader = createLoader(async (ctx) => {
|
|
911
|
-
// ctx.params: { slug: string }
|
|
912
|
-
// ctx.get("user"): User | undefined (from Rango.Vars)
|
|
913
|
-
// ctx.env.DB: D1Database (plain bindings from Rango.Env)
|
|
914
|
-
return { product: await fetchProduct(ctx.params.slug) };
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
// 6. Server: ctx.reverse for named routes
|
|
918
|
-
path("/product/:slug", (ctx) => {
|
|
919
|
-
return <Link to={ctx.reverse("shop")}>Back to Shop</Link>;
|
|
920
|
-
}, { name: "product" })
|
|
921
|
-
|
|
922
|
-
// 7. Client: useHref for mounted paths, href for absolute
|
|
923
|
-
"use client";
|
|
924
|
-
import { useHref, href, Link } from "@rangojs/router/client";
|
|
925
|
-
<Link to={href("/shop/product/widget")}>Widget</Link>
|
|
926
|
-
```
|
|
9
|
+
@rangojs/router provides end-to-end type safety for routes, parameters, and
|
|
10
|
+
environment. Without it: `ctx.reverse()`/`href()` accept any string (typos
|
|
11
|
+
404 at runtime, not compile time), `ctx.search`/`ctx.params` fall back to
|
|
12
|
+
loose `Record<string, string>`, and `ctx.env`/`ctx.get()` are untyped so a
|
|
13
|
+
missing binding surfaces as `undefined` in production instead of a build
|
|
14
|
+
error.
|
|
15
|
+
|
|
16
|
+
Each topic's full setup, code, and caveats live in a companion file linked
|
|
17
|
+
below. Read the one for your case.
|
|
18
|
+
|
|
19
|
+
## Routing table
|
|
20
|
+
|
|
21
|
+
| I need... | Topic | File |
|
|
22
|
+
| -------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | -------------------------------------------------------------- |
|
|
23
|
+
| Named routes, `.gen.ts` surfaces, `RegisteredRoutes` vs `GeneratedRouteMap`, tsconfig checklist | Router setup & generated route types | [`./generated-files-and-cli.md`](./generated-files-and-cli.md) |
|
|
24
|
+
| Type-safe `path()` names, `ctx.reverse()`, `href()`/`useHref()`, `Rango.PathResponse`, stable `path#export` identity | Route & href typing | [`./route-types.md`](./route-types.md) |
|
|
25
|
+
| Typed `search` schemas, `RouteSearchParams`/`RouteParams`, loader return types | Search params & loader typing | [`./params-and-search.md`](./params-and-search.md) |
|
|
26
|
+
| Typed `env`/bindings, `Rango.Vars`, `createVar()`, handle typing, loader/handle ref props, location state typing | Environment, context, and state typing | [`./env-and-bindings.md`](./env-and-bindings.md) |
|
|
27
|
+
| Multi-app / multi-router tsconfig setup, avoiding `GeneratedRouteMap` collisions | Multi-project setup & full walkthrough | [`./generated-files-and-cli.md`](./generated-files-and-cli.md) |
|
|
28
|
+
| Slow typecheck with many `include()` modules (instantiation blowup), wide `UrlPatterns<any>` annotations | Typecheck cost at route scale | [`./generated-files-and-cli.md`](./generated-files-and-cli.md) |
|
|
29
|
+
|
|
30
|
+
## Companion files
|
|
31
|
+
|
|
32
|
+
- [`./generated-files-and-cli.md`](./generated-files-and-cli.md) — Router
|
|
33
|
+
setup, the three route-typing surfaces (`GeneratedRouteMap` /
|
|
34
|
+
per-module `routes` / `RegisteredRoutes`), the single-app setup checklist,
|
|
35
|
+
`$$routeNames` vs `router.routeMap`, multi-project tsconfig setup, and the
|
|
36
|
+
complete end-to-end setup walkthrough.
|
|
37
|
+
- [`./route-types.md`](./route-types.md) — Type-safe route names, server
|
|
38
|
+
`ctx.reverse()`, client `href()`/`useHref()`, `Rango.Path`,
|
|
39
|
+
`Rango.PathResponse` (incl. overriding JSON/Flight serialization), and the
|
|
40
|
+
`path#export` stable identity scheme shared by loaders/handles/cached
|
|
41
|
+
functions/actions.
|
|
42
|
+
- [`./params-and-search.md`](./params-and-search.md) — Typed `search`
|
|
43
|
+
schemas on `path()`, `Handler<"name">` param/search inference,
|
|
44
|
+
`RouteSearchParams`/`RouteParams` utility types, and loader return-type
|
|
45
|
+
inference.
|
|
46
|
+
- [`./env-and-bindings.md`](./env-and-bindings.md) — Environment bindings
|
|
47
|
+
(`TEnv`) and `Rango.Env`/`Rango.Vars` registration, `createVar<T>()`
|
|
48
|
+
scoped context tokens, handle typing, passing loaders/handles as typed
|
|
49
|
+
props, and location state typing.
|
|
50
|
+
|
|
51
|
+
See `/links` for the full URL generation guide (per-module `*.gen.ts`,
|
|
52
|
+
`useReverse`).
|