@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.dc2bd2b4

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.
Files changed (253) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +2151 -846
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +57 -11
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +220 -30
  10. package/skills/caching/SKILL.md +116 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/document-cache/SKILL.md +78 -55
  13. package/skills/handler-use/SKILL.md +364 -0
  14. package/skills/hooks/SKILL.md +229 -20
  15. package/skills/host-router/SKILL.md +45 -20
  16. package/skills/i18n/SKILL.md +276 -0
  17. package/skills/intercept/SKILL.md +46 -4
  18. package/skills/layout/SKILL.md +28 -7
  19. package/skills/links/SKILL.md +247 -17
  20. package/skills/loader/SKILL.md +219 -9
  21. package/skills/middleware/SKILL.md +47 -12
  22. package/skills/migrate-nextjs/SKILL.md +562 -0
  23. package/skills/migrate-react-router/SKILL.md +769 -0
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +71 -6
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +242 -22
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +66 -9
  31. package/skills/route/SKILL.md +57 -4
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +751 -0
  34. package/skills/streams-and-websockets/SKILL.md +283 -0
  35. package/skills/testing/SKILL.md +647 -0
  36. package/skills/typesafety/SKILL.md +319 -27
  37. package/skills/use-cache/SKILL.md +34 -5
  38. package/skills/view-transitions/SKILL.md +294 -0
  39. package/src/__augment-tests__/augment.ts +81 -0
  40. package/src/__augment-tests__/augmented.check.ts +117 -0
  41. package/src/browser/action-coordinator.ts +53 -36
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/event-controller.ts +86 -70
  44. package/src/browser/history-state.ts +21 -0
  45. package/src/browser/index.ts +3 -3
  46. package/src/browser/navigation-bridge.ts +84 -11
  47. package/src/browser/navigation-client.ts +76 -28
  48. package/src/browser/navigation-store.ts +32 -9
  49. package/src/browser/navigation-transaction.ts +10 -28
  50. package/src/browser/partial-update.ts +64 -26
  51. package/src/browser/prefetch/cache.ts +129 -21
  52. package/src/browser/prefetch/fetch.ts +148 -16
  53. package/src/browser/prefetch/queue.ts +36 -5
  54. package/src/browser/rango-state.ts +53 -13
  55. package/src/browser/react/Link.tsx +30 -2
  56. package/src/browser/react/NavigationProvider.tsx +72 -31
  57. package/src/browser/react/filter-segment-order.ts +51 -7
  58. package/src/browser/react/index.ts +3 -0
  59. package/src/browser/react/location-state-shared.ts +175 -4
  60. package/src/browser/react/location-state.ts +39 -13
  61. package/src/browser/react/use-handle.ts +17 -9
  62. package/src/browser/react/use-navigation.ts +22 -2
  63. package/src/browser/react/use-params.ts +20 -8
  64. package/src/browser/react/use-reverse.ts +106 -0
  65. package/src/browser/react/use-router.ts +22 -2
  66. package/src/browser/react/use-segments.ts +11 -8
  67. package/src/browser/response-adapter.ts +25 -0
  68. package/src/browser/rsc-router.tsx +64 -22
  69. package/src/browser/scroll-restoration.ts +22 -14
  70. package/src/browser/segment-reconciler.ts +36 -14
  71. package/src/browser/segment-structure-assert.ts +2 -2
  72. package/src/browser/server-action-bridge.ts +23 -30
  73. package/src/browser/types.ts +21 -0
  74. package/src/build/collect-fallback-refs.ts +107 -0
  75. package/src/build/generate-manifest.ts +60 -35
  76. package/src/build/generate-route-types.ts +2 -0
  77. package/src/build/index.ts +2 -0
  78. package/src/build/route-trie.ts +52 -25
  79. package/src/build/route-types/codegen.ts +4 -4
  80. package/src/build/route-types/include-resolution.ts +1 -1
  81. package/src/build/route-types/per-module-writer.ts +7 -4
  82. package/src/build/route-types/router-processing.ts +55 -14
  83. package/src/build/route-types/scan-filter.ts +1 -1
  84. package/src/build/route-types/source-scan.ts +118 -0
  85. package/src/build/runtime-discovery.ts +9 -20
  86. package/src/cache/cache-scope.ts +28 -42
  87. package/src/cache/cf/cf-cache-store.ts +54 -13
  88. package/src/client.rsc.tsx +3 -0
  89. package/src/client.tsx +92 -182
  90. package/src/context-var.ts +5 -5
  91. package/src/decode-loader-results.ts +36 -0
  92. package/src/errors.ts +30 -1
  93. package/src/handle.ts +26 -13
  94. package/src/host/index.ts +2 -2
  95. package/src/host/router.ts +129 -57
  96. package/src/host/types.ts +31 -2
  97. package/src/host/utils.ts +1 -1
  98. package/src/href-client.ts +140 -20
  99. package/src/index.rsc.ts +9 -4
  100. package/src/index.ts +53 -15
  101. package/src/loader-store.ts +500 -0
  102. package/src/loader.rsc.ts +2 -5
  103. package/src/loader.ts +3 -10
  104. package/src/missing-id-error.ts +68 -0
  105. package/src/outlet-context.ts +1 -1
  106. package/src/prerender.ts +4 -4
  107. package/src/response-utils.ts +37 -0
  108. package/src/reverse.ts +65 -36
  109. package/src/route-content-wrapper.tsx +6 -28
  110. package/src/route-definition/dsl-helpers.ts +384 -257
  111. package/src/route-definition/helper-factories.ts +29 -139
  112. package/src/route-definition/helpers-types.ts +100 -28
  113. package/src/route-definition/resolve-handler-use.ts +6 -0
  114. package/src/route-definition/use-item-types.ts +32 -0
  115. package/src/route-types.ts +26 -41
  116. package/src/router/basename.ts +14 -0
  117. package/src/router/content-negotiation.ts +15 -2
  118. package/src/router/error-handling.ts +1 -1
  119. package/src/router/handler-context.ts +21 -38
  120. package/src/router/intercept-resolution.ts +4 -18
  121. package/src/router/lazy-includes.ts +8 -8
  122. package/src/router/loader-resolution.ts +19 -2
  123. package/src/router/manifest.ts +22 -13
  124. package/src/router/match-api.ts +4 -3
  125. package/src/router/match-handlers.ts +63 -20
  126. package/src/router/match-middleware/cache-lookup.ts +44 -91
  127. package/src/router/match-middleware/cache-store.ts +3 -2
  128. package/src/router/match-result.ts +53 -32
  129. package/src/router/metrics.ts +1 -1
  130. package/src/router/middleware-types.ts +15 -26
  131. package/src/router/middleware.ts +99 -84
  132. package/src/router/pattern-matching.ts +101 -17
  133. package/src/router/prerender-match.ts +1 -1
  134. package/src/router/preview-match.ts +3 -1
  135. package/src/router/request-classification.ts +4 -28
  136. package/src/router/revalidation.ts +58 -2
  137. package/src/router/router-interfaces.ts +45 -28
  138. package/src/router/router-options.ts +40 -1
  139. package/src/router/router-registry.ts +2 -5
  140. package/src/router/segment-resolution/fresh.ts +27 -6
  141. package/src/router/segment-resolution/revalidation.ts +147 -106
  142. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  143. package/src/router/substitute-pattern-params.ts +56 -0
  144. package/src/router/telemetry.ts +99 -0
  145. package/src/router/trie-matching.ts +18 -13
  146. package/src/router/types.ts +8 -0
  147. package/src/router/url-params.ts +49 -0
  148. package/src/router.ts +38 -23
  149. package/src/rsc/handler-context.ts +2 -2
  150. package/src/rsc/handler.ts +28 -69
  151. package/src/rsc/helpers.ts +91 -43
  152. package/src/rsc/index.ts +1 -1
  153. package/src/rsc/origin-guard.ts +28 -10
  154. package/src/rsc/progressive-enhancement.ts +4 -0
  155. package/src/rsc/response-route-handler.ts +46 -53
  156. package/src/rsc/rsc-rendering.ts +35 -51
  157. package/src/rsc/runtime-warnings.ts +9 -10
  158. package/src/rsc/server-action.ts +17 -37
  159. package/src/rsc/ssr-setup.ts +16 -0
  160. package/src/rsc/types.ts +8 -2
  161. package/src/search-params.ts +4 -4
  162. package/src/segment-content-promise.ts +67 -0
  163. package/src/segment-loader-promise.ts +122 -0
  164. package/src/segment-system.tsx +132 -116
  165. package/src/serialize.ts +243 -0
  166. package/src/server/context.ts +143 -53
  167. package/src/server/cookie-store.ts +28 -4
  168. package/src/server/request-context.ts +20 -42
  169. package/src/ssr/index.tsx +5 -1
  170. package/src/static-handler.ts +1 -1
  171. package/src/testing/cache-status.ts +166 -0
  172. package/src/testing/collect-handle.ts +63 -0
  173. package/src/testing/dispatch.ts +440 -0
  174. package/src/testing/dom.entry.ts +22 -0
  175. package/src/testing/e2e/fixture.ts +154 -0
  176. package/src/testing/e2e/index.ts +149 -0
  177. package/src/testing/e2e/matchers.ts +51 -0
  178. package/src/testing/e2e/page-helpers.ts +272 -0
  179. package/src/testing/e2e/parity.ts +306 -0
  180. package/src/testing/e2e/server.ts +183 -0
  181. package/src/testing/flight-matchers.ts +104 -0
  182. package/src/testing/flight-runtime.d.ts +21 -0
  183. package/src/testing/flight.entry.ts +22 -0
  184. package/src/testing/flight.ts +182 -0
  185. package/src/testing/generated-routes.ts +223 -0
  186. package/src/testing/index.ts +105 -0
  187. package/src/testing/internal/context.ts +193 -0
  188. package/src/testing/render-route.tsx +536 -0
  189. package/src/testing/run-loader.ts +296 -0
  190. package/src/testing/run-middleware.ts +170 -0
  191. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  192. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  193. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  194. package/src/testing/vitest-stubs/version.ts +5 -0
  195. package/src/testing/vitest.ts +183 -0
  196. package/src/types/global-namespace.ts +39 -26
  197. package/src/types/handler-context.ts +68 -50
  198. package/src/types/index.ts +1 -0
  199. package/src/types/loader-types.ts +5 -6
  200. package/src/types/request-scope.ts +126 -0
  201. package/src/types/route-entry.ts +11 -0
  202. package/src/types/segments.ts +35 -2
  203. package/src/urls/include-helper.ts +34 -67
  204. package/src/urls/index.ts +0 -3
  205. package/src/urls/path-helper-types.ts +41 -7
  206. package/src/urls/path-helper.ts +17 -52
  207. package/src/urls/pattern-types.ts +36 -19
  208. package/src/urls/response-types.ts +22 -29
  209. package/src/urls/type-extraction.ts +26 -116
  210. package/src/urls/urls-function.ts +1 -5
  211. package/src/use-loader.tsx +413 -42
  212. package/src/vite/debug.ts +185 -0
  213. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  214. package/src/vite/discovery/discover-routers.ts +101 -51
  215. package/src/vite/discovery/discovery-errors.ts +194 -0
  216. package/src/vite/discovery/gate-state.ts +171 -0
  217. package/src/vite/discovery/prerender-collection.ts +67 -26
  218. package/src/vite/discovery/route-types-writer.ts +40 -84
  219. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  220. package/src/vite/discovery/state.ts +33 -0
  221. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  222. package/src/vite/index.ts +2 -0
  223. package/src/vite/plugin-types.ts +67 -0
  224. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  225. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  226. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  227. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  228. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  229. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  230. package/src/vite/plugins/expose-action-id.ts +54 -30
  231. package/src/vite/plugins/expose-id-utils.ts +12 -8
  232. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  233. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  234. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  235. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  236. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  237. package/src/vite/plugins/performance-tracks.ts +29 -25
  238. package/src/vite/plugins/use-cache-transform.ts +65 -50
  239. package/src/vite/plugins/version-injector.ts +39 -23
  240. package/src/vite/plugins/version-plugin.ts +59 -2
  241. package/src/vite/plugins/virtual-entries.ts +2 -2
  242. package/src/vite/rango.ts +116 -29
  243. package/src/vite/router-discovery.ts +750 -100
  244. package/src/vite/utils/ast-handler-extract.ts +15 -15
  245. package/src/vite/utils/banner.ts +1 -1
  246. package/src/vite/utils/bundle-analysis.ts +4 -2
  247. package/src/vite/utils/client-chunks.ts +190 -0
  248. package/src/vite/utils/forward-user-plugins.ts +193 -0
  249. package/src/vite/utils/manifest-utils.ts +21 -5
  250. package/src/vite/utils/package-resolution.ts +41 -1
  251. package/src/vite/utils/prerender-utils.ts +21 -6
  252. package/src/vite/utils/shared-utils.ts +107 -26
  253. package/src/browser/action-response-classifier.ts +0 -99
@@ -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 like `href()`, `ValidPaths`, and `PathResponse`.
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 RSCRouter {
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 path-based response utilities read from `RegisteredRoutes`, so if
131
- you want them typed globally you should augment:
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 RSCRouter {
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 module augmentation
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 RSCRouter {
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 RSCRouter.Vars
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 RSCRouter {
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 RSCRouter.Env
224
- // ctx.get("user") is typed from global RSCRouter.Vars
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.
@@ -287,6 +452,12 @@ type SP = RouteSearchParams<"search">;
287
452
  type P = RouteParams<"blogPost">;
288
453
  // { slug: string }
289
454
 
455
+ // Optional URL params (`:slug?`) resolve to `string | undefined`
456
+ // because absent segments are omitted from `ctx.params` at runtime.
457
+ type C = RouteParams<"checkout">;
458
+ // { step?: string }
459
+ // → ctx.params.step is `string | undefined`; use `?? "default"` to coalesce.
460
+
290
461
  // Use in component props
291
462
  interface SearchResultsProps {
292
463
  params: RouteSearchParams<"search">;
@@ -319,6 +490,19 @@ export const NamedRoutes = {
319
490
  } as const;
320
491
  ```
321
492
 
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.
505
+
322
506
  ## Loader Type Safety
323
507
 
324
508
  Loaders have typed return values:
@@ -408,9 +592,9 @@ export function PaginationLayout(ctx: any) {
408
592
  }
409
593
  ```
410
594
 
411
- ### Why not just use RSCRouter.Vars?
595
+ ### Why not just use Rango.Vars?
412
596
 
413
- `RSCRouter.Vars` (via module augmentation) provides app-global typing for
597
+ `Rango.Vars` (via global namespace augmentation) provides app-global typing for
414
598
  `ctx.get("key")` / `ctx.set("key", value)`. It works for middleware state
415
599
  shared app-wide. `createVar<T>()` is for route-local or feature-scoped
416
600
  context -- the producer and consumer import the same token, creating a
@@ -462,9 +646,11 @@ export const ProductLoader = createLoader(async (ctx) => {
462
646
  });
463
647
 
464
648
  // Built-in Breadcrumbs — or any custom handle created with createHandle()
649
+ ```
465
650
 
651
+ ```tsx
466
652
  // Client component — typeof infers all generics
467
- ("use client");
653
+ "use client";
468
654
  import { useLoader, useHandle, type Breadcrumbs } from "@rangojs/router/client";
469
655
  import type { ProductLoader } from "../loaders";
470
656
 
@@ -485,6 +671,42 @@ RSC Flight serialization calls `toJSON()` on both loaders and handles,
485
671
  sending only `{ __brand, $$id }` to the client. The hooks recover the
486
672
  full functionality from module-level registries.
487
673
 
674
+ ## Stable identity: `path#export`
675
+
676
+ Loaders, handles, cached functions (`functionId`), and server actions
677
+ (`actionId`) all share one identity scheme: `{modulePath}#{exportName}`,
678
+ injected at build by the `exposeInternalIds` and `exposeActionId` Vite plugins.
679
+ This is also the identity React server actions carry across the Flight boundary,
680
+ which is why a `revalidate()` predicate sees an action as a `path#export` string:
681
+
682
+ ```typescript
683
+ revalidate(
684
+ ({ actionId }) => actionId === "src/actions/cart.ts#addToCart" || undefined,
685
+ );
686
+ ```
687
+
688
+ `actionId` is the only stable reference React exposes across the Flight boundary,
689
+ so it stays as the floor and escape hatch. The hand-written-string surface
690
+ (`actionId?.includes("cart.ts#")`) is brittle: a renamed action or moved file
691
+ silently stops matching with no compile error. Prefer **`ctx.isAction()`** in a
692
+ revalidate predicate — it resolves the action's id from an imported reference, so
693
+ a rename is a type error in one place instead of silent drift:
694
+
695
+ ```ts
696
+ import { addToCart, removeFromCart } from "./actions/cart";
697
+ import * as CartActions from "./actions/cart";
698
+
699
+ revalidate((ctx) => ctx.isAction(addToCart) || undefined); // one action
700
+ revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
701
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any action in the module
702
+ ```
703
+
704
+ `ctx.isAction()` (only available on the revalidate predicate's context) returns a
705
+ raw boolean — combine with `|| undefined` for the "revalidate on match, else
706
+ defer" intent. It resolves the reference the same way the router derives
707
+ `actionId` (`$id` in production, `$$id` in dev), so matching
708
+ works in both modes. `actionId` stays available for advanced cases.
709
+
488
710
  ## Location State Type Safety
489
711
 
490
712
  ```typescript
@@ -520,9 +742,37 @@ function ProductHeader() {
520
742
 
521
743
  ## Multi-Project tsconfig Setup
522
744
 
523
- For monorepos or multi-app setups, use a shared base tsconfig. Each app only needs
524
- to extend the base and add its `router.tsx` to `files` so TypeScript picks up the
525
- global type declarations (like `RSCRouter.Env`).
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.
526
776
 
527
777
  ```jsonc
528
778
  // tsconfig.base.json (root)
@@ -561,10 +811,49 @@ global type declarations (like `RSCRouter.Env`).
561
811
  }
562
812
  ```
563
813
 
564
- The `files` array ensures `router.tsx` (which contains `declare global { namespace RSCRouter { interface Env; interface Vars } }`)
565
- is always included in the compilation even if nothing directly imports it. Route types come from the
566
- auto-generated `*.named-routes.gen.ts` file (via `rango generate`), not from manual declaration.
567
- Each app gets its own typed environment without interfering with other apps.
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
+ ```
568
857
 
569
858
  ## Complete Type-Safe Setup
570
859
 
@@ -600,7 +889,7 @@ const router = createRouter<AppBindings>({
600
889
 
601
890
  // Register bindings and variables globally for implicit typing
602
891
  declare global {
603
- namespace RSCRouter {
892
+ namespace Rango {
604
893
  interface Env extends AppBindings {}
605
894
  interface Vars extends AppVariables {}
606
895
  }
@@ -611,13 +900,16 @@ export default router;
611
900
 
612
901
  // 4. Run `npx rango generate src/router.tsx` to generate
613
902
  // router.named-routes.gen.ts (auto-registers GeneratedRouteMap globally).
614
- // 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.
615
907
 
616
908
  // 5. loaders/*.ts - Type-safe loaders
617
909
  export const ProductLoader = createLoader(async (ctx) => {
618
910
  // ctx.params: { slug: string }
619
- // ctx.get("user"): User | undefined (from RSCRouter.Vars)
620
- // ctx.env.DB: D1Database (plain bindings from RSCRouter.Env)
911
+ // ctx.get("user"): User | undefined (from Rango.Vars)
912
+ // ctx.env.DB: D1Database (plain bindings from Rango.Env)
621
913
  return { product: await fetchProduct(ctx.params.slug) };
622
914
  });
623
915
 
@@ -68,7 +68,10 @@ createRouter({
68
68
 
69
69
  - `"use cache"` (no name) resolves to `default`.
70
70
  - `"use cache: short"` resolves to the `short` profile.
71
- - Unknown profile names throw at build/boot time.
71
+ - Unknown profile names throw at runtime, on the first invocation of the cached
72
+ function (the Vite transform does not validate names at build/boot). The error
73
+ is actionable -- it names the missing profile and shows the `createRouter({
74
+ cacheProfiles: { ... } })` entry to add.
72
75
 
73
76
  ## Cache Key
74
77
 
@@ -77,18 +80,33 @@ use-cache:{functionId}:{serializedArgs}
77
80
  ```
78
81
 
79
82
  - `functionId` -- stable ID from Vite transform (module path + export name).
80
- - `serializedArgs` -- non-tainted arguments serialized via RSC `encodeReply()`.
83
+ - `serializedArgs` -- key-generating arguments serialized via RSC `encodeReply()`.
84
+
85
+ When there are no key-generating arguments, the key has no trailing colon -- it is
86
+ just `use-cache:{functionId}`.
81
87
 
82
88
  Different functions always produce different cache keys, even for the same route.
83
89
  This is important for intercepted routes -- the path handler and intercept handler
84
90
  each have their own `functionId` and therefore their own cache entries.
85
91
 
92
+ ### Route context is folded into the key
93
+
94
+ The tainted `ctx` object is excluded from arg serialization (see below), but
95
+ route-identifying fields read off it are extracted into `serializedArgs`:
96
+ `url.host`, route name (`_routeName`), `pathname`, `params`, response type
97
+ (`_responseType`), and the user-facing sorted search params (internal `_rsc*`/`__`
98
+ params excluded). The same cached function called with `ctx` on different routes,
99
+ param combinations, hosts, response types, or query variants therefore produces
100
+ distinct cache entries -- not one shared entry.
101
+
86
102
  ## Tainted Arguments (ctx, env, req)
87
103
 
88
104
  Request-scoped objects are branded with `Symbol.for('rango:nocache')` at creation.
89
105
  When detected:
90
106
 
91
107
  1. **Excluded from cache key** -- request-scoped, not meaningful for keying.
108
+ (The route-identifying fields read off `ctx` are still folded in -- see
109
+ "Route context is folded into the key" above.)
92
110
  2. **Handle data captured on miss** -- side effects via `ctx.use(Handle)` are recorded.
93
111
  3. **Handle data replayed on hit** -- restored into the current request's HandleStore.
94
112
 
@@ -122,12 +140,16 @@ const data = await getCachedData(locale); // locale is now in the cache key
122
140
  These ctx methods **throw** inside a `"use cache"` function because their effects
123
141
  are lost on cache hit (the function body is skipped):
124
142
 
125
- - `ctx.set()` / `ctx.get()` for passing values to children
143
+ - `ctx.set()` for passing values to children
126
144
  - `ctx.header()`
127
145
  - `ctx.setTheme()`
128
146
  - `ctx.setLocationState()`
129
147
  - `ctx.onResponse()`
130
148
 
149
+ `ctx.get()` is **not** exec-guarded inside `"use cache"` -- it is a read, so it is
150
+ safe. (It only throws when reading a non-cacheable variable inside the separate
151
+ route-level `cache()` DSL boundary.)
152
+
131
153
  The error message recommends two alternatives:
132
154
 
133
155
  1. Extract the data fetch into a separate cached function and call ctx methods outside it.
@@ -304,8 +326,15 @@ export async function getProducts() {
304
326
  ## Backing Store
305
327
 
306
328
  Writes to the same `SegmentCacheStore` as `cache()` DSL, `Static()`, and `Prerender()`.
307
- One store, one configuration, one invalidation API. Tag-based invalidation
308
- (`revalidateTag`) works across all mechanisms.
329
+ One store, one configuration.
330
+
331
+ Cache entries (and `cacheProfiles`) accept an optional `tags` field, but the
332
+ built-in stores (`MemorySegmentCacheStore`, `CFCacheStore`) do not yet index or
333
+ invalidate by tag -- tags are passed through to the store and otherwise ignored.
334
+ Tag-based invalidation (`revalidateTag`) is a forward-looking API that requires a
335
+ custom store with secondary indices. Today entries expire by TTL/SWR. The separate
336
+ `revalidate()` export is the client-update axis (which segments re-render on a
337
+ navigation or action), not a cache bust.
309
338
 
310
339
  ## Interaction with Other Caching
311
340