@rangojs/router 0.0.0-experimental.107 → 0.0.0-experimental.109
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/README.md +4 -4
- package/dist/bin/rango.js +16 -16
- package/dist/vite/index.js +146 -150
- package/package.json +6 -6
- package/skills/hooks/SKILL.md +2 -0
- package/skills/links/SKILL.md +13 -1
- package/skills/loader/SKILL.md +1 -1
- package/skills/middleware/SKILL.md +3 -3
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/prerender/SKILL.md +13 -13
- package/skills/rango/SKILL.md +9 -0
- package/skills/response-routes/SKILL.md +58 -9
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/typesafety/SKILL.md +273 -31
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/react/location-state-shared.ts +3 -3
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/rsc-router.tsx +14 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/build/generate-manifest.ts +3 -3
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +3 -3
- package/src/build/route-types/router-processing.ts +4 -4
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/client.tsx +4 -7
- package/src/errors.ts +1 -1
- package/src/handle.ts +2 -2
- package/src/href-client.ts +136 -19
- package/src/index.rsc.ts +4 -4
- package/src/index.ts +2 -2
- package/src/loader.rsc.ts +1 -1
- package/src/loader.ts +1 -1
- package/src/prerender.ts +4 -4
- package/src/route-definition/dsl-helpers.ts +2 -2
- package/src/route-definition/helpers-types.ts +2 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/lazy-includes.ts +2 -2
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +1 -1
- package/src/router/prerender-match.ts +1 -1
- package/src/router/router-interfaces.ts +34 -28
- package/src/router/router-options.ts +1 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +2 -2
- package/src/router/segment-resolution/revalidation.ts +2 -2
- package/src/router.ts +13 -16
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/index.ts +1 -1
- package/src/rsc/types.ts +2 -2
- package/src/search-params.ts +4 -4
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +16 -16
- package/src/static-handler.ts +1 -1
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +3 -3
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/pattern-types.ts +34 -0
- package/src/urls/type-extraction.ts +6 -1
- package/src/use-loader.tsx +6 -4
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +3 -3
- package/src/vite/discovery/discovery-errors.ts +1 -1
- package/src/vite/discovery/prerender-collection.ts +19 -25
- package/src/vite/discovery/route-types-writer.ts +3 -3
- package/src/vite/discovery/state.ts +4 -4
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
- package/src/vite/plugins/expose-action-id.ts +2 -2
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +33 -9
- package/src/vite/plugins/expose-internal-ids.ts +1 -1
- package/src/vite/plugins/performance-tracks.ts +12 -16
- package/src/vite/plugins/use-cache-transform.ts +1 -1
- package/src/vite/plugins/version-plugin.ts +2 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +11 -11
- package/src/vite/router-discovery.ts +26 -29
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/forward-user-plugins.ts +46 -17
- package/src/vite/utils/shared-utils.ts +26 -22
|
@@ -37,7 +37,28 @@ available globally.
|
|
|
37
37
|
- `typeof router.routeMap` — the real merged route map from your router
|
|
38
38
|
instance, including response-route metadata such as `{ path, response }`.
|
|
39
39
|
- `RegisteredRoutes` — manual global hook for exposing `typeof router.routeMap`
|
|
40
|
-
to utilities
|
|
40
|
+
to global utilities that need the exact router-builder map, especially
|
|
41
|
+
`Rango.PathResponse`.
|
|
42
|
+
|
|
43
|
+
### Generated Route Type Surfaces
|
|
44
|
+
|
|
45
|
+
There are three distinct typing surfaces. They are **not** interchangeable —
|
|
46
|
+
pick the one that matches what you need to type:
|
|
47
|
+
|
|
48
|
+
| Surface | Source | Scope | Gives | Does not give |
|
|
49
|
+
| ------------------- | ---------------------------------------- | ------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
50
|
+
| `GeneratedRouteMap` | `router.named-routes.gen.ts` (auto) | global | route names, path params, search schemas | response/MIME payloads |
|
|
51
|
+
| `routes` | per-module `*.gen.ts` (`rango generate`) | local | local names, params, search | the global app map |
|
|
52
|
+
| `RegisteredRoutes` | manual `extends typeof router.routeMap` | global | paths, params, **response payloads** | the `Handler`/`Prerender` default (those read `GeneratedRouteMap` to avoid a `router.tsx` cycle) |
|
|
53
|
+
|
|
54
|
+
Key consequence: `href()` and the ambient `Rango.Path` type are typed from
|
|
55
|
+
whichever map is present — they prefer `RegisteredRoutes` when you wire it, otherwise fall back to
|
|
56
|
+
the auto-generated `GeneratedRouteMap`, so **`rango generate` alone gives you
|
|
57
|
+
path-checked `href()`** with no manual augmentation. Response and MIME payload
|
|
58
|
+
inference is the exception: it comes only from `typeof router.routeMap` (via
|
|
59
|
+
`RegisteredRoutes`), because `GeneratedRouteMap` carries paths + search but no
|
|
60
|
+
payloads — so `Rango.PathResponse` resolves to `ResponseEnvelope<never>` until you wire
|
|
61
|
+
`RegisteredRoutes`.
|
|
41
62
|
|
|
42
63
|
Recommended setup:
|
|
43
64
|
|
|
@@ -50,7 +71,7 @@ import type { AppBindings, AppVars } from "./env";
|
|
|
50
71
|
export const router = createRouter<AppBindings>({}).routes(urlpatterns);
|
|
51
72
|
|
|
52
73
|
declare global {
|
|
53
|
-
namespace
|
|
74
|
+
namespace Rango {
|
|
54
75
|
interface Env extends AppBindings {}
|
|
55
76
|
interface Vars extends AppVars {}
|
|
56
77
|
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
@@ -58,6 +79,54 @@ declare global {
|
|
|
58
79
|
}
|
|
59
80
|
```
|
|
60
81
|
|
|
82
|
+
### Single-App Setup Checklist
|
|
83
|
+
|
|
84
|
+
For one app, keep the ambient types, generated named-routes file, and router
|
|
85
|
+
instance in the same TypeScript program:
|
|
86
|
+
|
|
87
|
+
```jsonc
|
|
88
|
+
// tsconfig.json
|
|
89
|
+
{
|
|
90
|
+
"compilerOptions": {
|
|
91
|
+
"strict": true,
|
|
92
|
+
"moduleResolution": "bundler",
|
|
93
|
+
"jsx": "react-jsx",
|
|
94
|
+
"noEmit": true,
|
|
95
|
+
},
|
|
96
|
+
"include": ["src"],
|
|
97
|
+
"files": ["src/router.tsx"],
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Then generate the route types from the router file:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx rango generate src/router.tsx
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This creates `src/router.named-routes.gen.ts`, which augments
|
|
108
|
+
`Rango.GeneratedRouteMap`. Keep that generated file committed with the router
|
|
109
|
+
source. The `files` entry keeps `router.tsx` in the program even when nothing
|
|
110
|
+
imports it directly, so `Rango.Env`, `Rango.Vars`, and optional
|
|
111
|
+
`Rango.RegisteredRoutes` augmentation are visible to handlers, loaders, actions,
|
|
112
|
+
and client helpers.
|
|
113
|
+
|
|
114
|
+
### Named Routes, `$$routeNames`, And `router.routeMap`
|
|
115
|
+
|
|
116
|
+
There are two runtime/type surfaces with similar names:
|
|
117
|
+
|
|
118
|
+
- `router.named-routes.gen.ts` exports `NamedRoutes` and augments
|
|
119
|
+
`Rango.GeneratedRouteMap`. The Vite plugin imports that file internally and
|
|
120
|
+
injects it as `$$routeNames` so `router.reverse` has the static route-name map.
|
|
121
|
+
App code should not pass or import `$$routeNames` directly.
|
|
122
|
+
- `router.routeMap` is the public router instance property for type extraction.
|
|
123
|
+
Use `typeof router.routeMap` when augmenting `Rango.RegisteredRoutes` for
|
|
124
|
+
global response payload helpers such as `Rango.PathResponse`.
|
|
125
|
+
|
|
126
|
+
Do not document or use a public `router.routeNames` API unless one is
|
|
127
|
+
intentionally added. Today, the public extraction surface is `router.routeMap`;
|
|
128
|
+
the generated file and `$$routeNames` are build machinery.
|
|
129
|
+
|
|
61
130
|
## Route Definition with Type-Safe Names
|
|
62
131
|
|
|
63
132
|
```typescript
|
|
@@ -127,17 +196,107 @@ function ShopNav() {
|
|
|
127
196
|
}
|
|
128
197
|
```
|
|
129
198
|
|
|
130
|
-
`href()` and
|
|
131
|
-
|
|
199
|
+
`href()` and the `Rango.Path` type read from `RegisteredRoutes` when you augment
|
|
200
|
+
it, otherwise from the auto-generated `GeneratedRouteMap` — so `rango generate`
|
|
201
|
+
alone type-checks `href()` paths with no manual augmentation. The augmentation
|
|
202
|
+
below is only needed for **`Rango.PathResponse`** (response-payload inference), which
|
|
203
|
+
`GeneratedRouteMap` cannot provide:
|
|
132
204
|
|
|
133
205
|
```typescript
|
|
134
206
|
declare global {
|
|
135
|
-
namespace
|
|
207
|
+
namespace Rango {
|
|
136
208
|
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
137
209
|
}
|
|
138
210
|
}
|
|
139
211
|
```
|
|
140
212
|
|
|
213
|
+
For wrapper helpers, type the path parameter as `Rango.Path`. It is ambient (no
|
|
214
|
+
import) and shares `href()`'s compile-time path checking, so a wrapper stays in
|
|
215
|
+
sync with your routes automatically:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { href } from "@rangojs/router/client";
|
|
219
|
+
|
|
220
|
+
export const appHref = (path: Rango.Path): string => href(path);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
For response-route payloads, `Rango.PathResponse<T>` is the ambient lookup. It
|
|
224
|
+
accepts a route _pattern_ **or** a concrete path, so it also serves as the return
|
|
225
|
+
type of a typed `fetch` wrapper. It only resolves once `RegisteredRoutes` carries
|
|
226
|
+
response metadata:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { href } from "@rangojs/router/client";
|
|
230
|
+
|
|
231
|
+
type Product = Rango.PathResponse<"/api/products/:id">; // by pattern
|
|
232
|
+
type Same = Rango.PathResponse<"/api/products/42">; // by concrete path
|
|
233
|
+
|
|
234
|
+
// Response inferred from the concrete path passed in:
|
|
235
|
+
async function get<T extends Rango.Path>(
|
|
236
|
+
path: T,
|
|
237
|
+
): Promise<Rango.PathResponse<T>> {
|
|
238
|
+
return fetch(href(path)).then((r) => r.json());
|
|
239
|
+
}
|
|
240
|
+
const product = await get("/api/products/42"); // ResponseEnvelope<Product>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Pattern keys (`/:id`) match exactly; a concrete path under a _nested_ dynamic
|
|
244
|
+
route can match several patterns and union their responses.
|
|
245
|
+
|
|
246
|
+
`Rango.PathResponse` describes the JSON **wire** shape, not the handler's raw
|
|
247
|
+
return. A `path.json()` handler returning `{ createdAt: Date }` resolves here to
|
|
248
|
+
`ResponseEnvelope<{ createdAt: string }>`, matching what `r.json()` yields. This
|
|
249
|
+
is applied via the ambient `Rango.JsonSerialize<T>` transform (`Date -> string`,
|
|
250
|
+
honors `toJSON()`, drops functions/`undefined`, `bigint -> never`). A separate
|
|
251
|
+
`Rango.FlightSerialize<T>` models the higher-fidelity RSC Flight boundary
|
|
252
|
+
(loaders / RSC props, where `Date` is preserved) — do **not** use it for
|
|
253
|
+
`path.json()`.
|
|
254
|
+
|
|
255
|
+
### Overriding serialization globally
|
|
256
|
+
|
|
257
|
+
For your own types, the zero-config way to control the JSON wire shape is a
|
|
258
|
+
`toJSON()` method — `Rango.JsonSerialize` honors it, and it matches the runtime
|
|
259
|
+
exactly (`JSON.stringify` calls `toJSON()`):
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
class Money {
|
|
263
|
+
constructor(private cents: number) {}
|
|
264
|
+
toJSON(): number {
|
|
265
|
+
return this.cents;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Rango.JsonSerialize<Money> is number; Rango.PathResponse reflects it.
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
To override a transform for types you **don't** own (or for the Flight boundary,
|
|
272
|
+
which has no `toJSON()`), augment its override slot. Because `Rango.JsonSerialize`
|
|
273
|
+
/ `Rango.FlightSerialize` are type _aliases_ (TS can't merge those), you provide a
|
|
274
|
+
single member that is your **complete** transform, delegating to the built-in for
|
|
275
|
+
the cases you don't change:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
declare global {
|
|
279
|
+
namespace Rango {
|
|
280
|
+
interface JsonSerializeOverride<T> {
|
|
281
|
+
app: T extends Decimal ? string : Rango.JsonSerializeBuiltin<T>;
|
|
282
|
+
}
|
|
283
|
+
interface FlightSerializeOverride<T> {
|
|
284
|
+
app: T extends Money ? number : Rango.FlightSerializeBuiltin<T>;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Rango.JsonSerialize<Decimal> -> string; Rango.FlightSerialize<Money> -> number;
|
|
289
|
+
// everything else stays on the built-in, recursively (nested fields too).
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Rules: provide **exactly one** member (the slot is read as
|
|
293
|
+
`Override<T>[keyof Override<T>]`, so multiple members union and conflict).
|
|
294
|
+
Overrides win over `toJSON()` and apply at every nesting level. Caveat for JSON:
|
|
295
|
+
the `path.json()` runtime is plain `JSON.stringify`, which only honors `toJSON()`,
|
|
296
|
+
so a `JsonSerializeOverride` that disagrees with what the runtime emits will lie —
|
|
297
|
+
prefer `toJSON()` for your own types and use the slot only for types you can't
|
|
298
|
+
modify.
|
|
299
|
+
|
|
141
300
|
See `/links` for full URL generation guide.
|
|
142
301
|
|
|
143
302
|
## Environment Type Setup
|
|
@@ -155,7 +314,7 @@ export interface AppBindings {
|
|
|
155
314
|
AI: Ai;
|
|
156
315
|
}
|
|
157
316
|
|
|
158
|
-
// Variables set by middleware — declared via
|
|
317
|
+
// Variables set by middleware — declared via global namespace augmentation
|
|
159
318
|
export interface AppVariables {
|
|
160
319
|
user?: { id: string; email: string; role: string };
|
|
161
320
|
requestId?: string;
|
|
@@ -175,7 +334,7 @@ const router = createRouter<AppBindings>({
|
|
|
175
334
|
|
|
176
335
|
// Register bindings and variables globally for implicit typing
|
|
177
336
|
declare global {
|
|
178
|
-
namespace
|
|
337
|
+
namespace Rango {
|
|
179
338
|
interface Env extends AppBindings {}
|
|
180
339
|
interface Vars extends AppVariables {}
|
|
181
340
|
}
|
|
@@ -196,7 +355,7 @@ export const authMiddleware: Middleware = async (ctx, next) => {
|
|
|
196
355
|
// loaders - typed context
|
|
197
356
|
export const UserLoader = createLoader(async (ctx) => {
|
|
198
357
|
const db = ctx.env.DB; // D1Database (plain bindings)
|
|
199
|
-
const userId = ctx.get("user")?.id; // from
|
|
358
|
+
const userId = ctx.get("user")?.id; // from Rango.Vars
|
|
200
359
|
return db.prepare("SELECT * FROM users WHERE id = ?").bind(userId).first();
|
|
201
360
|
});
|
|
202
361
|
```
|
|
@@ -208,7 +367,7 @@ Register environment types globally for implicit typing:
|
|
|
208
367
|
```typescript
|
|
209
368
|
// router.tsx
|
|
210
369
|
declare global {
|
|
211
|
-
namespace
|
|
370
|
+
namespace Rango {
|
|
212
371
|
interface Env extends AppBindings {}
|
|
213
372
|
interface Vars extends AppVariables {}
|
|
214
373
|
}
|
|
@@ -220,8 +379,8 @@ Now handlers have typed context without explicit imports:
|
|
|
220
379
|
```typescript
|
|
221
380
|
// In loaders
|
|
222
381
|
export const DashboardLoader = createLoader(async (ctx) => {
|
|
223
|
-
// ctx.env.DB is typed from global
|
|
224
|
-
// ctx.get("user") is typed from global
|
|
382
|
+
// ctx.env.DB is typed from global Rango.Env
|
|
383
|
+
// ctx.get("user") is typed from global Rango.Vars
|
|
225
384
|
const user = ctx.get("user");
|
|
226
385
|
return { user };
|
|
227
386
|
});
|
|
@@ -259,15 +418,21 @@ This avoids circular references because `Handler` defaults to `GeneratedRouteMap
|
|
|
259
418
|
(from `router.named-routes.gen.ts`) instead of `RegisteredRoutes` (which depends on `router.tsx`).
|
|
260
419
|
|
|
261
420
|
You can also pass an explicit route map for per-module isolation (opt-in,
|
|
262
|
-
after running `npx rango generate`)
|
|
421
|
+
after running `npx rango generate`). With a local map, the route name is
|
|
422
|
+
**dot-prefixed** so params and search resolve from `routes`, not the global map:
|
|
263
423
|
|
|
264
424
|
```typescript
|
|
265
425
|
import type { Handler } from "@rangojs/router";
|
|
266
426
|
import type { routes } from "./urls.gen.js";
|
|
267
427
|
|
|
268
|
-
export const SearchPage: Handler<"search", routes> = (ctx) => { ... };
|
|
428
|
+
export const SearchPage: Handler<".search", routes> = (ctx) => { ... };
|
|
269
429
|
```
|
|
270
430
|
|
|
431
|
+
Note the difference: `Handler<"search">` (no dot) resolves against the global
|
|
432
|
+
`GeneratedRouteMap`; `Handler<".search", routes>` resolves against the local
|
|
433
|
+
`routes` map. Mixing them — `Handler<"search", routes>` — silently ignores
|
|
434
|
+
`routes` for param/search inference and only uses it for local `ctx.reverse(".x")`.
|
|
435
|
+
|
|
271
436
|
Supported types: `"string"`, `"number"`, `"boolean"`, with `?` suffix for optional.
|
|
272
437
|
Values are automatically coerced from query string (e.g., `"2"` becomes `2` for numbers).
|
|
273
438
|
Routes without a `search` schema keep the standard `URLSearchParams` behavior.
|
|
@@ -325,11 +490,18 @@ export const NamedRoutes = {
|
|
|
325
490
|
} as const;
|
|
326
491
|
```
|
|
327
492
|
|
|
328
|
-
You never open a `.gen.ts` by hand
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
493
|
+
You never open a `.gen.ts` by hand. Treat the generated types as call-site
|
|
494
|
+
honesty checks, not modules to read:
|
|
495
|
+
|
|
496
|
+
- **Do not import `router.named-routes.gen.ts` directly**, and don't reach for
|
|
497
|
+
`Rango.GeneratedRouteMap`. It is the whole-app manifest, auto-wired
|
|
498
|
+
globally — `Handler<"name">` and `ctx.reverse("name")` already see it.
|
|
499
|
+
- **Per-module `*.gen.ts` imports are fine** — they are the opt-in local-route
|
|
500
|
+
pattern for `useReverse(routes)` and explicit local handler typing
|
|
501
|
+
(`Handler<".name", routes>`). See `/links`.
|
|
502
|
+
|
|
503
|
+
If a type error points at a generated map instead of your call site, that's a
|
|
504
|
+
smell — fix the call site (or regenerate), never edit the generated file.
|
|
333
505
|
|
|
334
506
|
## Loader Type Safety
|
|
335
507
|
|
|
@@ -420,9 +592,9 @@ export function PaginationLayout(ctx: any) {
|
|
|
420
592
|
}
|
|
421
593
|
```
|
|
422
594
|
|
|
423
|
-
### Why not just use
|
|
595
|
+
### Why not just use Rango.Vars?
|
|
424
596
|
|
|
425
|
-
`
|
|
597
|
+
`Rango.Vars` (via global namespace augmentation) provides app-global typing for
|
|
426
598
|
`ctx.get("key")` / `ctx.set("key", value)`. It works for middleware state
|
|
427
599
|
shared app-wide. `createVar<T>()` is for route-local or feature-scoped
|
|
428
600
|
context -- the producer and consumer import the same token, creating a
|
|
@@ -570,9 +742,37 @@ function ProductHeader() {
|
|
|
570
742
|
|
|
571
743
|
## Multi-Project tsconfig Setup
|
|
572
744
|
|
|
573
|
-
For monorepos or multi-app setups,
|
|
574
|
-
|
|
575
|
-
|
|
745
|
+
For monorepos or multi-app setups, each app should have its own TypeScript
|
|
746
|
+
program. Do not typecheck two Rango apps with different `Rango.Env`,
|
|
747
|
+
`Rango.Vars`, or `Rango.RegisteredRoutes` declarations in one tsconfig, because
|
|
748
|
+
ambient global interfaces merge across the whole program.
|
|
749
|
+
|
|
750
|
+
### Multiple routers in one program
|
|
751
|
+
|
|
752
|
+
`Rango.GeneratedRouteMap` is a **single global interface**. Each router's
|
|
753
|
+
generated `router.named-routes.gen.ts` augments it, so two routers in the **same
|
|
754
|
+
TS program** that define overlapping route names (e.g. both have a `home`) make
|
|
755
|
+
the augmentations collide:
|
|
756
|
+
|
|
757
|
+
```text
|
|
758
|
+
Interface 'GeneratedRouteMap' cannot simultaneously extend ...
|
|
759
|
+
Named property 'home' ... are not identical.
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
This is the multi-router / host-router case. Resolve it by:
|
|
763
|
+
|
|
764
|
+
- **Separate TS programs** — give each router its own tsconfig (as below) so only
|
|
765
|
+
one generated map is in scope per program. Recommended.
|
|
766
|
+
- **Unique route-name prefixes** — name routes per router (`appA.home`,
|
|
767
|
+
`appB.home`) so the merged global map has no duplicate keys.
|
|
768
|
+
|
|
769
|
+
A single global generated map is a single-router convenience; global named-route
|
|
770
|
+
typing across multiple routers in one program is not supported today (it would
|
|
771
|
+
need per-router scoping in the generated map).
|
|
772
|
+
|
|
773
|
+
Use a shared base tsconfig for common compiler options, then make every app
|
|
774
|
+
tsconfig include its own source tree, its own `router.tsx`, and the generated
|
|
775
|
+
`router.named-routes.gen.ts` that lives beside that router.
|
|
576
776
|
|
|
577
777
|
```jsonc
|
|
578
778
|
// tsconfig.base.json (root)
|
|
@@ -611,10 +811,49 @@ global type declarations (like `RSCRouter.Env`).
|
|
|
611
811
|
}
|
|
612
812
|
```
|
|
613
813
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
814
|
+
Run generation per app:
|
|
815
|
+
|
|
816
|
+
```bash
|
|
817
|
+
npx rango generate apps/shop/src/router.tsx
|
|
818
|
+
npx rango generate apps/blog/src/router.tsx
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
If an app has multiple tsconfigs (`tsconfig.app.json`, `tsconfig.test.json`,
|
|
822
|
+
`tsconfig.worker.json`), every tsconfig that typechecks Rango handlers,
|
|
823
|
+
components, loaders, actions, or client navigation must see the same app-local
|
|
824
|
+
type surfaces:
|
|
825
|
+
|
|
826
|
+
```jsonc
|
|
827
|
+
// apps/shop/tsconfig.test.json
|
|
828
|
+
{
|
|
829
|
+
"extends": "./tsconfig.json",
|
|
830
|
+
"include": ["src", "tests"],
|
|
831
|
+
"files": ["src/router.tsx"],
|
|
832
|
+
}
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
The `files` array ensures `router.tsx` is always included even if nothing
|
|
836
|
+
directly imports it. The generated `router.named-routes.gen.ts` is normally
|
|
837
|
+
covered by `include: ["src"]`; if a tsconfig uses a narrow `include`, add the
|
|
838
|
+
generated file explicitly. Each app gets its own typed environment and named
|
|
839
|
+
route map without interfering with other apps.
|
|
840
|
+
|
|
841
|
+
For response and MIME payload lookup in each app, augment `RegisteredRoutes`
|
|
842
|
+
inside that app's router file:
|
|
843
|
+
|
|
844
|
+
```typescript
|
|
845
|
+
// apps/shop/src/router.tsx
|
|
846
|
+
export const router = createRouter<ShopEnv>({ document: Document }).routes(
|
|
847
|
+
urlpatterns,
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
declare global {
|
|
851
|
+
namespace Rango {
|
|
852
|
+
interface Env extends ShopEnv {}
|
|
853
|
+
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
```
|
|
618
857
|
|
|
619
858
|
## Complete Type-Safe Setup
|
|
620
859
|
|
|
@@ -650,7 +889,7 @@ const router = createRouter<AppBindings>({
|
|
|
650
889
|
|
|
651
890
|
// Register bindings and variables globally for implicit typing
|
|
652
891
|
declare global {
|
|
653
|
-
namespace
|
|
892
|
+
namespace Rango {
|
|
654
893
|
interface Env extends AppBindings {}
|
|
655
894
|
interface Vars extends AppVariables {}
|
|
656
895
|
}
|
|
@@ -661,13 +900,16 @@ export default router;
|
|
|
661
900
|
|
|
662
901
|
// 4. Run `npx rango generate src/router.tsx` to generate
|
|
663
902
|
// router.named-routes.gen.ts (auto-registers GeneratedRouteMap globally).
|
|
664
|
-
// No manual RegisteredRoutes declaration needed
|
|
903
|
+
// No manual RegisteredRoutes declaration is needed for named-route handlers,
|
|
904
|
+
// ctx.reverse, prerender, href(), or Rango.Path. Add `RegisteredRoutes
|
|
905
|
+
// extends typeof router.routeMap` when global response payload helpers such
|
|
906
|
+
// as Rango.PathResponse need the richer router.routeMap metadata.
|
|
665
907
|
|
|
666
908
|
// 5. loaders/*.ts - Type-safe loaders
|
|
667
909
|
export const ProductLoader = createLoader(async (ctx) => {
|
|
668
910
|
// ctx.params: { slug: string }
|
|
669
|
-
// ctx.get("user"): User | undefined (from
|
|
670
|
-
// ctx.env.DB: D1Database (plain bindings from
|
|
911
|
+
// ctx.get("user"): User | undefined (from Rango.Vars)
|
|
912
|
+
// ctx.env.DB: D1Database (plain bindings from Rango.Env)
|
|
671
913
|
return { product: await fetchProduct(ctx.params.slug) };
|
|
672
914
|
});
|
|
673
915
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulates a consumer augmenting the Rango global namespace, the way a real
|
|
3
|
+
* app does in router.tsx (Env/Vars) and via the generated router.named-routes.gen.ts
|
|
4
|
+
* (GeneratedRouteMap).
|
|
5
|
+
*
|
|
6
|
+
* This file is compiled ONLY by tsconfig.augment-check.json and is excluded from
|
|
7
|
+
* the main program, so the global augmentation here does not leak into the rest
|
|
8
|
+
* of the type tests, which assert the UNAUGMENTED fallbacks
|
|
9
|
+
* (see src/__tests__/augmentation-fallback-types.test.ts).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface TestBindings {
|
|
13
|
+
DB: { query: (sql: string) => string };
|
|
14
|
+
SECRET: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TestVars {
|
|
18
|
+
user?: { id: string; role: "admin" | "user" };
|
|
19
|
+
requestId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A userland domain type that controls its own JSON wire shape via `toJSON()`.
|
|
24
|
+
* This is the augmentation hook: `Rango.JsonSerialize` honors `toJSON()`, so a
|
|
25
|
+
* consumer adjusts how their type serializes with no registry API.
|
|
26
|
+
*/
|
|
27
|
+
export class Money {
|
|
28
|
+
constructor(public cents: number) {}
|
|
29
|
+
toJSON(): number {
|
|
30
|
+
return this.cents;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mirrors `typeof router.routeMap`: the same routes as the generated map, plus a
|
|
36
|
+
* response route whose payload carries a userland class (`Money`, with
|
|
37
|
+
* `toJSON()`) and a `Date` — used to assert `Rango.PathResponse` reports the
|
|
38
|
+
* serialized JSON wire shape.
|
|
39
|
+
*/
|
|
40
|
+
export interface TestRegisteredRoutes {
|
|
41
|
+
readonly home: "/";
|
|
42
|
+
readonly "blog.post": "/blog/:slug";
|
|
43
|
+
readonly search: {
|
|
44
|
+
readonly path: "/search";
|
|
45
|
+
readonly search: { readonly q: "string"; readonly page: "number?" };
|
|
46
|
+
};
|
|
47
|
+
readonly order: {
|
|
48
|
+
readonly path: "/orders/:id";
|
|
49
|
+
readonly response: { id: string; total: Money; placedAt: Date };
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
declare global {
|
|
54
|
+
namespace Rango {
|
|
55
|
+
interface Env extends TestBindings {}
|
|
56
|
+
interface Vars extends TestVars {}
|
|
57
|
+
interface RegisteredRoutes extends TestRegisteredRoutes {}
|
|
58
|
+
// Mirrors the shape emitted into router.named-routes.gen.ts: plain string
|
|
59
|
+
// patterns, plus { path, search } objects for routes with a search schema.
|
|
60
|
+
interface GeneratedRouteMap {
|
|
61
|
+
readonly home: "/";
|
|
62
|
+
readonly "blog.post": "/blog/:slug";
|
|
63
|
+
readonly search: {
|
|
64
|
+
readonly path: "/search";
|
|
65
|
+
readonly search: { readonly q: "string"; readonly page: "number?" };
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Userland serialization overrides: full-transform replacement that
|
|
70
|
+
// special-cases one type and delegates the rest to the built-in. Isolated to
|
|
71
|
+
// this augment-check program, so the main suite's built-in behavior (e.g.
|
|
72
|
+
// JsonSerialize<bigint> = never) is unaffected — demonstrating overrides are
|
|
73
|
+
// per-project augmentation.
|
|
74
|
+
interface JsonSerializeOverride<T> {
|
|
75
|
+
app: T extends bigint ? string : Rango.JsonSerializeBuiltin<T>;
|
|
76
|
+
}
|
|
77
|
+
interface FlightSerializeOverride<T> {
|
|
78
|
+
app: T extends Money ? number : Rango.FlightSerializeBuiltin<T>;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-only assertions for the AUGMENTED Rango namespace.
|
|
3
|
+
*
|
|
4
|
+
* Pins the type-safety contract a consumer gets after augmenting Env, Vars, and
|
|
5
|
+
* GeneratedRouteMap. A regression in the fallback chains (global-namespace.ts)
|
|
6
|
+
* turns these into tsc errors. Run via tsconfig.augment-check.json.
|
|
7
|
+
*/
|
|
8
|
+
import "./augment.js";
|
|
9
|
+
import type { Handler, RouteParams, RouteSearchParams } from "../index.js";
|
|
10
|
+
import type { DefaultRouteName } from "../types/global-namespace.js";
|
|
11
|
+
import { href } from "../href-client.js";
|
|
12
|
+
import type { ResponseEnvelope } from "../urls.js";
|
|
13
|
+
import type { Money, TestBindings } from "./augment.js";
|
|
14
|
+
|
|
15
|
+
type Expect<T extends true> = T;
|
|
16
|
+
type Equal<A, B> =
|
|
17
|
+
(<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
|
|
18
|
+
? true
|
|
19
|
+
: false;
|
|
20
|
+
|
|
21
|
+
// Env: ctx.env resolves to the augmented bindings, not `unknown`/`any`.
|
|
22
|
+
const envHandler: Handler<"home"> = (ctx) => {
|
|
23
|
+
type _envIsBindings = Expect<Equal<typeof ctx.env, TestBindings>>;
|
|
24
|
+
ctx.env.DB.query("select 1");
|
|
25
|
+
// @ts-expect-error - unknown binding is rejected once Env is augmented
|
|
26
|
+
ctx.env.MISSING();
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
void envHandler;
|
|
30
|
+
|
|
31
|
+
// Vars: ctx.get is keyed by the augmented Vars.
|
|
32
|
+
const varsHandler: Handler<"home"> = (ctx) => {
|
|
33
|
+
const user = ctx.get("user");
|
|
34
|
+
type _userTyped = Expect<
|
|
35
|
+
Equal<typeof user, { id: string; role: "admin" | "user" } | undefined>
|
|
36
|
+
>;
|
|
37
|
+
// @ts-expect-error - unknown var key is rejected once Vars is augmented
|
|
38
|
+
ctx.get("nope");
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
41
|
+
void varsHandler;
|
|
42
|
+
|
|
43
|
+
// routeName narrows to the generated route names.
|
|
44
|
+
type _routeName = Expect<
|
|
45
|
+
Equal<DefaultRouteName, "home" | "blog.post" | "search">
|
|
46
|
+
>;
|
|
47
|
+
|
|
48
|
+
// RouteParams / RouteSearchParams resolve from the generated map with no
|
|
49
|
+
// explicit route map argument.
|
|
50
|
+
type _params = Expect<Equal<RouteParams<"blog.post">, { slug: string }>>;
|
|
51
|
+
type _search = Expect<
|
|
52
|
+
Equal<RouteSearchParams<"search">, { q: string | undefined; page?: number }>
|
|
53
|
+
>;
|
|
54
|
+
|
|
55
|
+
// href / Rango.Path read GeneratedRouteMap even without a manual RegisteredRoutes
|
|
56
|
+
// augmentation — this is the core of the "rango generate alone enables typed
|
|
57
|
+
// href()" guarantee. The paths below come from the generated map in augment.ts.
|
|
58
|
+
href("/");
|
|
59
|
+
href("/blog/anything");
|
|
60
|
+
href("/search");
|
|
61
|
+
// @ts-expect-error - path is not in the generated route map
|
|
62
|
+
href("/not-a-route");
|
|
63
|
+
|
|
64
|
+
// Rango.Path is the ambient input type for wrapper functions around href().
|
|
65
|
+
// No import needed — it reads the same generated map href() does.
|
|
66
|
+
function wrappedHref(path: Rango.Path): string {
|
|
67
|
+
return href(path);
|
|
68
|
+
}
|
|
69
|
+
const arrowHref = (path: Rango.Path): string => href(path);
|
|
70
|
+
|
|
71
|
+
wrappedHref("/blog/anything");
|
|
72
|
+
arrowHref("/search?q=hello");
|
|
73
|
+
// @ts-expect-error - wrapper preserves the same generated-map validation
|
|
74
|
+
wrappedHref("/not-a-route");
|
|
75
|
+
|
|
76
|
+
// Userland serialization augmentation: a consumer's custom class adjusts its JSON
|
|
77
|
+
// wire shape via toJSON(), and Rango.PathResponse reports the serialized payload
|
|
78
|
+
// (Money -> number, Date -> string) — both by pattern and by concrete path.
|
|
79
|
+
type _orderWireByPattern = Expect<
|
|
80
|
+
Equal<
|
|
81
|
+
Rango.PathResponse<"/orders/:id">,
|
|
82
|
+
ResponseEnvelope<{ id: string; total: number; placedAt: string }>
|
|
83
|
+
>
|
|
84
|
+
>;
|
|
85
|
+
type _orderWireByPath = Expect<
|
|
86
|
+
Equal<
|
|
87
|
+
Rango.PathResponse<"/orders/42">,
|
|
88
|
+
ResponseEnvelope<{ id: string; total: number; placedAt: string }>
|
|
89
|
+
>
|
|
90
|
+
>;
|
|
91
|
+
|
|
92
|
+
// Project serialization overrides: the consumer augments JsonSerializeOverride /
|
|
93
|
+
// FlightSerializeOverride (see augment.ts) with a full transform that delegates to
|
|
94
|
+
// the built-in, and the transforms honor it — winning over the built-in rules.
|
|
95
|
+
// bigint normally JSON-serializes to `never`; Money is a class React Flight would
|
|
96
|
+
// otherwise reject structurally. Delegation keeps every other type (incl. the
|
|
97
|
+
// /orders payload above) on the built-in behavior.
|
|
98
|
+
type _jsonOverride = Expect<Equal<Rango.JsonSerialize<bigint>, string>>;
|
|
99
|
+
type _jsonOverrideNested = Expect<
|
|
100
|
+
Equal<
|
|
101
|
+
Rango.JsonSerialize<{ id: bigint; name: string }>,
|
|
102
|
+
{ id: string; name: string }
|
|
103
|
+
>
|
|
104
|
+
>;
|
|
105
|
+
type _flightOverride = Expect<Equal<Rango.FlightSerialize<Money>, number>>;
|
|
106
|
+
|
|
107
|
+
// Reference the top-level assertion aliases so they are unambiguously evaluated.
|
|
108
|
+
export type _Assertions = [
|
|
109
|
+
_routeName,
|
|
110
|
+
_params,
|
|
111
|
+
_search,
|
|
112
|
+
_orderWireByPattern,
|
|
113
|
+
_orderWireByPath,
|
|
114
|
+
_jsonOverride,
|
|
115
|
+
_jsonOverrideNested,
|
|
116
|
+
_flightOverride,
|
|
117
|
+
];
|
package/src/browser/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
|
-
// Browser Module - Browser entry point for
|
|
2
|
+
// Browser Module - Browser entry point for Rango
|
|
3
3
|
// ============================================================================
|
|
4
4
|
//
|
|
5
5
|
// Usage:
|
|
6
|
-
// import { initBrowserApp,
|
|
6
|
+
// import { initBrowserApp, Rango } from "rsc-router/browser";
|
|
7
7
|
//
|
|
8
8
|
// For React components (Link, useNavigation, etc.):
|
|
9
9
|
// import { Link, useNavigation, useAction, href } from "rsc-router/client";
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
// Browser app initialization
|
|
14
14
|
export {
|
|
15
15
|
initBrowserApp,
|
|
16
|
-
|
|
16
|
+
Rango,
|
|
17
17
|
type InitBrowserAppOptions,
|
|
18
18
|
} from "./rsc-router.js";
|
|
@@ -125,7 +125,7 @@ export function createLocationState<TState>(
|
|
|
125
125
|
function getKey(): string {
|
|
126
126
|
if (!_key && process.env.NODE_ENV === "development") {
|
|
127
127
|
throw new Error(
|
|
128
|
-
"[
|
|
128
|
+
"[rango] createLocationState key not set. " +
|
|
129
129
|
"Make sure the exposeInternalIds Vite plugin is enabled and " +
|
|
130
130
|
"the state is exported with: export const MyState = createLocationState(...)",
|
|
131
131
|
);
|
|
@@ -176,7 +176,7 @@ export function createLocationState<TState>(
|
|
|
176
176
|
value: (value: TState): void => {
|
|
177
177
|
if (typeof window === "undefined") {
|
|
178
178
|
throw new Error(
|
|
179
|
-
"[
|
|
179
|
+
"[rango] LocationState.write() is client-only. " +
|
|
180
180
|
"It mutates window.history.state and cannot run on the server.",
|
|
181
181
|
);
|
|
182
182
|
}
|
|
@@ -195,7 +195,7 @@ export function createLocationState<TState>(
|
|
|
195
195
|
value: (): void => {
|
|
196
196
|
if (typeof window === "undefined") {
|
|
197
197
|
throw new Error(
|
|
198
|
-
"[
|
|
198
|
+
"[rango] LocationState.delete() is client-only. " +
|
|
199
199
|
"It mutates window.history.state and cannot run on the server.",
|
|
200
200
|
);
|
|
201
201
|
}
|