@rangojs/router 0.0.0-experimental.d20dd405 → 0.0.0-experimental.d98a8e9d

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 (242) hide show
  1. package/README.md +8 -8
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +917 -500
  5. package/package.json +55 -11
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/bundle-analysis/SKILL.md +159 -0
  8. package/skills/cache-guide/SKILL.md +220 -30
  9. package/skills/caching/SKILL.md +116 -8
  10. package/skills/composability/SKILL.md +27 -2
  11. package/skills/document-cache/SKILL.md +78 -55
  12. package/skills/handler-use/SKILL.md +1 -1
  13. package/skills/hooks/SKILL.md +196 -21
  14. package/skills/host-router/SKILL.md +45 -20
  15. package/skills/intercept/SKILL.md +1 -4
  16. package/skills/layout/SKILL.md +4 -7
  17. package/skills/links/SKILL.md +22 -10
  18. package/skills/loader/SKILL.md +166 -23
  19. package/skills/middleware/SKILL.md +13 -9
  20. package/skills/migrate-nextjs/SKILL.md +1 -1
  21. package/skills/mime-routes/SKILL.md +27 -0
  22. package/skills/observability/SKILL.md +137 -0
  23. package/skills/parallel/SKILL.md +3 -6
  24. package/skills/prerender/SKILL.md +14 -33
  25. package/skills/rango/SKILL.md +243 -26
  26. package/skills/react-compiler/SKILL.md +168 -0
  27. package/skills/response-routes/SKILL.md +114 -47
  28. package/skills/route/SKILL.md +9 -4
  29. package/skills/router-setup/SKILL.md +3 -3
  30. package/skills/server-actions/SKILL.md +53 -41
  31. package/skills/testing/SKILL.md +128 -0
  32. package/skills/testing/bindings.md +89 -0
  33. package/skills/testing/cache-prerender.md +98 -0
  34. package/skills/testing/client-components.md +121 -0
  35. package/skills/testing/e2e-parity.md +124 -0
  36. package/skills/testing/flight.md +89 -0
  37. package/skills/testing/handles.md +127 -0
  38. package/skills/testing/loader.md +108 -0
  39. package/skills/testing/middleware.md +97 -0
  40. package/skills/testing/render-handler.md +102 -0
  41. package/skills/testing/response-routes.md +94 -0
  42. package/skills/testing/reverse-and-types.md +83 -0
  43. package/skills/testing/server-actions.md +89 -0
  44. package/skills/testing/server-tree.md +128 -0
  45. package/skills/testing/setup.md +120 -0
  46. package/skills/typesafety/SKILL.md +310 -26
  47. package/skills/use-cache/SKILL.md +34 -5
  48. package/skills/view-transitions/SKILL.md +85 -3
  49. package/src/__augment-tests__/augment.ts +81 -0
  50. package/src/__augment-tests__/augmented.check.ts +116 -0
  51. package/src/browser/action-coordinator.ts +53 -36
  52. package/src/browser/event-controller.ts +42 -66
  53. package/src/browser/history-state.ts +21 -0
  54. package/src/browser/index.ts +3 -3
  55. package/src/browser/navigation-bridge.ts +9 -67
  56. package/src/browser/navigation-client.ts +68 -83
  57. package/src/browser/navigation-store.ts +7 -8
  58. package/src/browser/navigation-transaction.ts +10 -28
  59. package/src/browser/partial-update.ts +8 -16
  60. package/src/browser/prefetch/cache.ts +58 -27
  61. package/src/browser/prefetch/fetch.ts +92 -33
  62. package/src/browser/react/NavigationProvider.tsx +55 -65
  63. package/src/browser/react/location-state-shared.ts +175 -4
  64. package/src/browser/react/location-state.ts +39 -13
  65. package/src/browser/react/use-handle.ts +17 -9
  66. package/src/browser/react/use-params.ts +3 -4
  67. package/src/browser/react/use-reverse.ts +19 -12
  68. package/src/browser/react/use-router.ts +14 -1
  69. package/src/browser/response-adapter.ts +32 -1
  70. package/src/browser/rsc-router.tsx +35 -16
  71. package/src/browser/scroll-restoration.ts +30 -25
  72. package/src/browser/segment-structure-assert.ts +2 -2
  73. package/src/browser/server-action-bridge.ts +23 -30
  74. package/src/browser/types.ts +2 -0
  75. package/src/build/collect-fallback-refs.ts +107 -0
  76. package/src/build/generate-manifest.ts +60 -35
  77. package/src/build/generate-route-types.ts +2 -0
  78. package/src/build/index.ts +8 -1
  79. package/src/build/prefix-tree-utils.ts +123 -0
  80. package/src/build/route-trie.ts +43 -0
  81. package/src/build/route-types/codegen.ts +4 -4
  82. package/src/build/route-types/include-resolution.ts +1 -1
  83. package/src/build/route-types/per-module-writer.ts +7 -4
  84. package/src/build/route-types/router-processing.ts +55 -14
  85. package/src/build/route-types/scan-filter.ts +1 -1
  86. package/src/build/route-types/source-scan.ts +118 -0
  87. package/src/build/runtime-discovery.ts +9 -20
  88. package/src/cache/cache-scope.ts +28 -42
  89. package/src/cache/cf/cf-cache-store.ts +49 -6
  90. package/src/client.tsx +9 -30
  91. package/src/context-var.ts +5 -5
  92. package/src/decode-loader-results.ts +36 -0
  93. package/src/errors.ts +30 -4
  94. package/src/handle.ts +32 -14
  95. package/src/host/index.ts +2 -2
  96. package/src/host/router.ts +129 -57
  97. package/src/host/types.ts +31 -2
  98. package/src/host/utils.ts +1 -1
  99. package/src/href-client.ts +136 -20
  100. package/src/index.rsc.ts +7 -6
  101. package/src/index.ts +14 -8
  102. package/src/loader-store.ts +500 -0
  103. package/src/loader.rsc.ts +25 -7
  104. package/src/loader.ts +16 -9
  105. package/src/missing-id-error.ts +68 -0
  106. package/src/prerender.ts +27 -6
  107. package/src/response-utils.ts +9 -0
  108. package/src/reverse.ts +16 -13
  109. package/src/route-content-wrapper.tsx +6 -28
  110. package/src/route-definition/dsl-helpers.ts +238 -263
  111. package/src/route-definition/helper-factories.ts +29 -139
  112. package/src/route-definition/helpers-types.ts +37 -14
  113. package/src/route-definition/use-item-types.ts +32 -0
  114. package/src/route-types.ts +19 -41
  115. package/src/router/basename.ts +14 -0
  116. package/src/router/content-negotiation.ts +15 -2
  117. package/src/router/error-handling.ts +1 -1
  118. package/src/router/find-match.ts +54 -6
  119. package/src/router/intercept-resolution.ts +4 -18
  120. package/src/router/lazy-includes.ts +35 -16
  121. package/src/router/loader-resolution.ts +79 -36
  122. package/src/router/manifest.ts +19 -6
  123. package/src/router/match-handlers.ts +62 -20
  124. package/src/router/match-middleware/cache-lookup.ts +44 -91
  125. package/src/router/match-middleware/cache-store.ts +3 -2
  126. package/src/router/match-result.ts +32 -30
  127. package/src/router/metrics.ts +1 -1
  128. package/src/router/middleware-types.ts +1 -1
  129. package/src/router/middleware.ts +46 -78
  130. package/src/router/pattern-matching.ts +15 -2
  131. package/src/router/prerender-match.ts +1 -1
  132. package/src/router/preview-match.ts +3 -1
  133. package/src/router/request-classification.ts +4 -28
  134. package/src/router/revalidation.ts +43 -1
  135. package/src/router/router-interfaces.ts +45 -28
  136. package/src/router/router-options.ts +40 -1
  137. package/src/router/router-registry.ts +2 -5
  138. package/src/router/segment-resolution/fresh.ts +19 -6
  139. package/src/router/segment-resolution/revalidation.ts +19 -6
  140. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  141. package/src/router/telemetry.ts +99 -0
  142. package/src/router/trie-matching.ts +22 -3
  143. package/src/router/types.ts +8 -0
  144. package/src/router.ts +51 -28
  145. package/src/rsc/handler-context.ts +2 -2
  146. package/src/rsc/handler.ts +20 -65
  147. package/src/rsc/helpers.ts +22 -2
  148. package/src/rsc/index.ts +1 -1
  149. package/src/rsc/manifest-init.ts +28 -41
  150. package/src/rsc/origin-guard.ts +28 -10
  151. package/src/rsc/response-error.ts +79 -12
  152. package/src/rsc/response-route-handler.ts +43 -60
  153. package/src/rsc/rsc-rendering.ts +27 -53
  154. package/src/rsc/runtime-warnings.ts +9 -10
  155. package/src/rsc/server-action.ts +13 -37
  156. package/src/rsc/ssr-setup.ts +16 -0
  157. package/src/rsc/types.ts +2 -2
  158. package/src/runtime-env.ts +18 -0
  159. package/src/search-params.ts +4 -4
  160. package/src/segment-system.tsx +64 -49
  161. package/src/serialize.ts +243 -0
  162. package/src/server/context.ts +150 -51
  163. package/src/server/cookie-store.ts +28 -4
  164. package/src/server/request-context.ts +57 -9
  165. package/src/static-handler.ts +25 -3
  166. package/src/testing/cache-status.ts +166 -0
  167. package/src/testing/collect-handle.ts +63 -0
  168. package/src/testing/dispatch.ts +581 -0
  169. package/src/testing/dom.entry.ts +22 -0
  170. package/src/testing/e2e/fixture.ts +188 -0
  171. package/src/testing/e2e/index.ts +149 -0
  172. package/src/testing/e2e/matchers.ts +51 -0
  173. package/src/testing/e2e/page-helpers.ts +272 -0
  174. package/src/testing/e2e/parity.ts +326 -0
  175. package/src/testing/e2e/server.ts +195 -0
  176. package/src/testing/flight-matchers.ts +110 -0
  177. package/src/testing/flight-normalize.ts +38 -0
  178. package/src/testing/flight-runtime.d.ts +57 -0
  179. package/src/testing/flight-tree.ts +682 -0
  180. package/src/testing/flight.entry.ts +51 -0
  181. package/src/testing/flight.ts +234 -0
  182. package/src/testing/generated-routes.ts +223 -0
  183. package/src/testing/index.ts +106 -0
  184. package/src/testing/internal/context.ts +304 -0
  185. package/src/testing/internal/flight-client-globals.ts +30 -0
  186. package/src/testing/internal/seed-vars.ts +42 -0
  187. package/src/testing/render-handler.ts +323 -0
  188. package/src/testing/render-route.tsx +590 -0
  189. package/src/testing/run-loader.ts +363 -0
  190. package/src/testing/run-middleware.ts +205 -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 +285 -0
  196. package/src/types/global-namespace.ts +39 -26
  197. package/src/types/handler-context.ts +56 -11
  198. package/src/types/index.ts +1 -0
  199. package/src/types/loader-types.ts +6 -3
  200. package/src/types/segments.ts +18 -1
  201. package/src/urls/include-helper.ts +10 -53
  202. package/src/urls/index.ts +1 -5
  203. package/src/urls/path-helper-types.ts +11 -3
  204. package/src/urls/path-helper.ts +17 -52
  205. package/src/urls/pattern-types.ts +36 -19
  206. package/src/urls/response-types.ts +20 -19
  207. package/src/urls/type-extraction.ts +58 -139
  208. package/src/urls/urls-function.ts +1 -5
  209. package/src/use-loader.tsx +413 -42
  210. package/src/vite/debug.ts +1 -0
  211. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  212. package/src/vite/discovery/discover-routers.ts +75 -72
  213. package/src/vite/discovery/discovery-errors.ts +194 -0
  214. package/src/vite/discovery/prerender-collection.ts +19 -25
  215. package/src/vite/discovery/route-types-writer.ts +40 -84
  216. package/src/vite/discovery/state.ts +33 -0
  217. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  218. package/src/vite/index.ts +2 -0
  219. package/src/vite/plugin-types.ts +67 -0
  220. package/src/vite/plugins/cjs-to-esm.ts +3 -7
  221. package/src/vite/plugins/client-ref-hashing.ts +12 -1
  222. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
  223. package/src/vite/plugins/expose-action-id.ts +2 -2
  224. package/src/vite/plugins/expose-id-utils.ts +12 -8
  225. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  226. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  227. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  228. package/src/vite/plugins/expose-internal-ids.ts +47 -67
  229. package/src/vite/plugins/performance-tracks.ts +12 -16
  230. package/src/vite/plugins/use-cache-transform.ts +13 -11
  231. package/src/vite/plugins/version-injector.ts +2 -12
  232. package/src/vite/plugins/version-plugin.ts +59 -2
  233. package/src/vite/plugins/virtual-entries.ts +2 -2
  234. package/src/vite/rango.ts +67 -15
  235. package/src/vite/router-discovery.ts +208 -63
  236. package/src/vite/utils/ast-handler-extract.ts +15 -15
  237. package/src/vite/utils/bundle-analysis.ts +4 -2
  238. package/src/vite/utils/client-chunks.ts +190 -0
  239. package/src/vite/utils/forward-user-plugins.ts +193 -0
  240. package/src/vite/utils/manifest-utils.ts +8 -59
  241. package/src/vite/utils/shared-utils.ts +107 -26
  242. package/src/browser/action-response-classifier.ts +0 -99
@@ -68,16 +68,16 @@ export const urlpatterns = urls(({ path, layout, include }) => [
68
68
 
69
69
  ## Available Tags
70
70
 
71
- | Tag | Usage | Handler returns | Auto-wrap |
72
- | -------- | --------------- | ------------------ | ------------------------ |
73
- | `json` | `path.json()` | plain object/array | `{ data: T }` envelope |
74
- | `text` | `path.text()` | string | text/plain Response |
75
- | `html` | `path.html()` | string | text/html Response |
76
- | `xml` | `path.xml()` | string | application/xml Response |
77
- | `md` | `path.md()` | string | text/markdown Response |
78
- | `image` | `path.image()` | Response | pass-through |
79
- | `stream` | `path.stream()` | Response | pass-through |
80
- | `any` | `path.any()` | Response | pass-through |
71
+ | Tag | Usage | Handler returns | Auto-wrap |
72
+ | -------- | --------------- | ------------------ | ----------------------------- |
73
+ | `json` | `path.json()` | plain object/array | bare JSON value (no envelope) |
74
+ | `text` | `path.text()` | string | text/plain Response |
75
+ | `html` | `path.html()` | string | text/html Response |
76
+ | `xml` | `path.xml()` | string | application/xml Response |
77
+ | `md` | `path.md()` | string | text/markdown Response |
78
+ | `image` | `path.image()` | Response | pass-through |
79
+ | `stream` | `path.stream()` | Response | pass-through |
80
+ | `any` | `path.any()` | Response | pass-through |
81
81
 
82
82
  ## ResponseHandlerContext
83
83
 
@@ -139,22 +139,31 @@ path.json(
139
139
  );
140
140
  ```
141
141
 
142
- ## JSON Envelope
142
+ ## JSON Wire Shape
143
143
 
144
- `path.json()` handlers return plain data. The framework auto-wraps it
145
- in a `ResponseEnvelope<T>` discriminated union:
144
+ `path.json()` handlers return plain data. The framework serializes the handler's
145
+ return value **verbatim** (no envelope) on success, and an RFC 9457 `problem+json`
146
+ body on error. Discriminate with `res.ok` / the HTTP status — there is no in-body
147
+ `data`/`error` union:
146
148
 
147
149
  ```typescript
148
- // Success: HTTP 200
149
- { "data": { "status": "ok", "timestamp": 1700000000 } }
150
-
151
- // Error: HTTP 404 (or whatever status RouterError specifies)
152
- { "error": { "message": "Product 999 not found", "code": "NOT_FOUND" } }
150
+ // Success: HTTP 200, content-type application/json
151
+ { "status": "ok", "timestamp": 1700000000 }
152
+
153
+ // Error: HTTP 404 (or whatever status RouterError specifies),
154
+ // content-type application/problem+json
155
+ {
156
+ "title": "Not Found",
157
+ "status": 404,
158
+ "detail": "Product 999 not found",
159
+ "code": "NOT_FOUND"
160
+ // "stack": included in development only
161
+ }
153
162
  ```
154
163
 
155
164
  ### Error Handling with RouterError
156
165
 
157
- Throw `RouterError` to return structured error envelopes:
166
+ Throw `RouterError` to return a structured `problem+json` body:
158
167
 
159
168
  ```typescript
160
169
  import { RouterError } from "@rangojs/router";
@@ -199,25 +208,27 @@ path.json(
199
208
 
200
209
  ## Client-Side Type Safety
201
210
 
202
- ### ResponseEnvelope and isResponseError
211
+ ### Discriminating success vs. error with res.ok
212
+
213
+ Success bodies are the bare value; error bodies are RFC 9457 `ProblemDetails`.
214
+ Branch on `res.ok` (or the HTTP status) — not an in-body union:
203
215
 
204
216
  ```typescript
205
217
  "use client";
206
- import type { ResponseEnvelope, ResponseError } from "@rangojs/router/client";
207
- import { isResponseError } from "@rangojs/router/client";
218
+ import type { ProblemDetails } from "@rangojs/router";
208
219
 
209
220
  // Fetch a typed response
210
221
  const res = await fetch("/api/products/1");
211
- const result: ResponseEnvelope<Product> = await res.json();
212
222
 
213
- if (isResponseError(result)) {
214
- // result.error: ResponseError (message, code?, type?)
215
- // result.data: undefined
216
- console.error(result.error.message);
223
+ if (!res.ok) {
224
+ // Error body: application/problem+json
225
+ const problem: ProblemDetails = await res.json();
226
+ // problem.detail: string, problem.code: string, problem.status: number
227
+ console.error(problem.code, problem.detail);
217
228
  } else {
218
- // result.data: Product
219
- // result.error: undefined
220
- console.log(result.data.name);
229
+ // Success body: the bare value (no envelope)
230
+ const product: Product = await res.json();
231
+ console.log(product.name);
221
232
  }
222
233
  ```
223
234
 
@@ -230,28 +241,78 @@ import type { RouteResponse } from "@rangojs/router";
230
241
 
231
242
  // From the apiPatterns module (before include)
232
243
  type HealthData = RouteResponse<typeof apiPatterns, "health">;
233
- // = ResponseEnvelope<{ status: string; timestamp: number }>
244
+ // = { status: string; timestamp: number }
234
245
 
235
246
  type ProductsData = RouteResponse<typeof apiPatterns, "products">;
236
- // = ResponseEnvelope<{ id: string; name: string; price: number }[]>
247
+ // = { id: string; name: string; price: number }[]
237
248
  ```
238
249
 
239
- ### PathResponse (global lookup by URL pattern)
250
+ `RouteResponse` is the bare success payload (the JSON wire shape) — the same value
251
+ a `fetch().then(r => r.json())` yields on a 2xx. Error bodies are `ProblemDetails`,
252
+ keyed off `res.ok` at runtime, not part of this type.
240
253
 
241
- Look up response type from the merged route map by URL pattern:
254
+ ### Rango.PathResponse (global lookup by URL pattern or concrete path)
255
+
256
+ `Rango.PathResponse` is ambient (no import) and reads from `RegisteredRoutes`,
257
+ which carries response payload metadata. That surface is **not** auto-wired —
258
+ without the augmentation below, `Rango.PathResponse` falls back to the generated
259
+ path/search map, or to a permissive map when nothing is generated. Either way, it
260
+ has no response payload metadata, so response routes resolve to `never`:
242
261
 
243
262
  ```typescript
244
- import type { PathResponse } from "@rangojs/router/client";
263
+ // router.tsx
264
+ export const router = createRouter({ document: Document }).routes(urlpatterns);
245
265
 
266
+ declare global {
267
+ namespace Rango {
268
+ interface RegisteredRoutes extends typeof router.routeMap {}
269
+ }
270
+ }
271
+ ```
272
+
273
+ With that in place, look up the response type by URL pattern (ambient, no import):
274
+
275
+ ```typescript
246
276
  // After include("/api", apiPatterns) in main urls
247
- type Health = PathResponse<"/api/health">;
248
- // = ResponseEnvelope<{ status: string; timestamp: number }>
277
+ type Health = Rango.PathResponse<"/api/health">;
278
+ // = { status: string; timestamp: number }
279
+
280
+ // RSC routes (no JSON payload) return never
281
+ type Home = Rango.PathResponse<"/">;
282
+ // = never
283
+ ```
249
284
 
250
- // RSC routes return ResponseEnvelope<never>
251
- type Home = PathResponse<"/">;
252
- // = ResponseEnvelope<never>
285
+ `Rango.PathResponse` also accepts a **concrete path**, so it types a `fetch`
286
+ wrapper whose response is inferred from the path you pass:
287
+
288
+ ```typescript
289
+ import { href } from "@rangojs/router/client";
290
+
291
+ async function get<T extends Rango.Path>(
292
+ path: T,
293
+ ): Promise<Rango.PathResponse<T>> {
294
+ return fetch(href(path)).then((r) => r.json());
295
+ }
296
+
297
+ const product = await get("/api/products/42"); // Product (bare value)
253
298
  ```
254
299
 
300
+ Pattern keys (`/:id`) match exactly; a concrete path under a _nested_ dynamic
301
+ route can match several patterns and union their responses.
302
+
303
+ `Rango.PathResponse` reports the JSON **wire** shape, not the handler's raw
304
+ return: `path.json()` serializes with `JSON.stringify`, so a handler returning
305
+ `{ createdAt: Date }` resolves to the bare `{ createdAt: string }`. This
306
+ runs through the ambient `Rango.JsonSerialize<T>` transform (`Date -> string`,
307
+ honors `toJSON()`, drops functions/`undefined`, `bigint -> never`). The
308
+ `RouteResponse` surface below applies the same `Rango.JsonSerialize` transform, so
309
+ both response lookups report the identical wire shape.
310
+
311
+ For local/scoped response typing without global augmentation, prefer
312
+ `RouteResponse<typeof patterns, "routeName">` (see the section above) — it reads
313
+ the response payload straight from the `urls()` patterns and needs no
314
+ `RegisteredRoutes` wiring.
315
+
255
316
  ### ParamsFor with Response Routes
256
317
 
257
318
  ```typescript
@@ -361,15 +422,17 @@ export const urlpatterns = urls(({ path, include }) => [
361
422
 
362
423
  ```typescript
363
424
  import type { RouteResponse } from "@rangojs/router";
364
- import type { PathResponse, ParamsFor } from "@rangojs/router/client";
425
+ import type { ParamsFor } from "@rangojs/router/client";
365
426
 
366
- // Scoped (before mount) -- use the module directly
427
+ // Scoped (before mount) -- use the module directly, no global wiring needed
367
428
  type Stats = RouteResponse<typeof blogApiPatterns, "stats">;
368
- // = ResponseEnvelope<{ views: number; visitors: number }>
429
+ // = { views: number; visitors: number }
369
430
 
370
- // After mounting -- names get prefixed
371
- type BlogStats = PathResponse<"/blog/api/stats">;
372
- // = ResponseEnvelope<{ views: number; visitors: number }>
431
+ // After mounting -- names get prefixed.
432
+ // Rango.PathResponse needs `RegisteredRoutes extends typeof router.routeMap` (see above),
433
+ // otherwise it resolves to never.
434
+ type BlogStats = Rango.PathResponse<"/blog/api/stats">;
435
+ // = { views: number; visitors: number }
373
436
 
374
437
  // Params work through nested includes
375
438
  type LikesParams = ParamsFor<"blog.api.likes">;
@@ -413,7 +476,11 @@ best-effort basis.
413
476
  1. `path.json()` tags the route at the trie level with a MIME type
414
477
  2. `coreRequestHandler()` checks the tag before the RSC pipeline
415
478
  3. Tagged routes short-circuit: handler runs, Response is returned directly
416
- 4. JSON routes auto-wrap return values in `{ data }` / `{ error }` envelope
479
+ 4. JSON routes serialize the return value verbatim (bare) on success; a thrown error becomes an RFC 9457 `problem+json` body (`application/problem+json`)
417
480
  5. Client-side navigation to response routes gets `X-RSC-Reload` header, triggering hard navigation
418
481
  6. Response types flow through `_responses` phantom type on `UrlPatterns`, propagated by `include()`
419
482
  7. When multiple routes share a URL pattern, the trie merges them for content negotiation (see `/mime-routes`)
483
+
484
+ ## Consuming response routes
485
+
486
+ To call your own response-route JSON APIs from first-party TypeScript with a typed client (typed params, typed payloads inferred from the handler, no `.data`, typed `ProblemDetails` errors), see `/api-client` — a copy-paste recipe over `RouteResponse` + `ExtractParams` + a client-safe path builder. External/third-party consumers use the plain wire directly: bare JSON on success, `application/problem+json` on error.
@@ -234,14 +234,22 @@ Cacheable vars (the default) can be read freely inside cache scopes.
234
234
 
235
235
  ### Revalidation Contracts for Handler Data
236
236
 
237
+ > **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
238
+ > It decides whether this segment re-runs and streams to the client on a
239
+ > navigation or action — never whether a cached value is stale. The cache
240
+ > decides hit/miss/ttl/swr independently and never reads `revalidate()`. See
241
+ > `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
242
+
237
243
  Handler-first guarantees apply within a single full render pass. For partial
238
244
  action revalidation, define named revalidation contracts and reuse them on both
239
245
  the producer route and the consumer child segments.
240
246
 
241
247
  ```typescript
242
248
  // revalidation-contracts.ts
249
+ // Defer (|| undefined), not ?? false: a hard `false` short-circuits the chain,
250
+ // so when the same segment composes multiple contracts the later ones never run.
243
251
  export const revalidateCheckoutData = ({ actionId }) =>
244
- actionId?.includes("src/actions/checkout.ts#") ?? false;
252
+ actionId?.includes("src/actions/checkout.ts#") || undefined;
245
253
 
246
254
  path("/checkout", CheckoutPage, { name: "checkout" }, () => [
247
255
  revalidate(revalidateCheckoutData), // producer (route handler) reruns
@@ -270,9 +278,6 @@ path("/checkout", CheckoutPage, { name: "checkout" }, () => [
270
278
  ]);
271
279
  ```
272
280
 
273
- For scope/revalidation guarantees and non-guarantees, see:
274
- [docs/execution-model.md](../../docs/internal/execution-model.md)
275
-
276
281
  ## Redirects
277
282
 
278
283
  ### Basic redirect
@@ -71,7 +71,7 @@ urls(
71
71
  ## Router Options
72
72
 
73
73
  ```typescript
74
- interface RSCRouterOptions<TEnv> {
74
+ interface RangoOptions<TEnv> {
75
75
  // URL patterns from urls() function
76
76
  urls: UrlPatterns;
77
77
 
@@ -405,7 +405,7 @@ interface AppBindings {
405
405
  KV: KVNamespace;
406
406
  }
407
407
 
408
- // Variables declared via module augmentation
408
+ // Variables declared via global namespace augmentation
409
409
  interface AppVariables {
410
410
  user?: { id: string; name: string };
411
411
  }
@@ -417,7 +417,7 @@ const router = createRouter<AppBindings>({
417
417
 
418
418
  // Register types globally for implicit typing
419
419
  declare global {
420
- namespace RSCRouter {
420
+ namespace Rango {
421
421
  interface Env extends AppBindings {}
422
422
  interface Vars extends AppVariables {}
423
423
  }
@@ -32,35 +32,37 @@ Actions mutate state; route handlers and loaders read the latest state. After
32
32
  an action finishes, Rango performs a server-side revalidation render for the
33
33
  matched route so the UI receives fresh segment output and loader data.
34
34
 
35
- The main control point is `revalidate(({ actionId }) => ...)` on the segment
36
- that owns the data. This applies to `path()` handlers, `layout()` handlers,
37
- `parallel()` slots, `intercept()` routes, and loader registrations:
35
+ The main control point is `revalidate((ctx) => ...)` on the segment that owns
36
+ the data. Match specific actions by imported reference with `ctx.isAction()`;
37
+ use raw `actionId` only when you intentionally need path or directory matching.
38
+ This applies to `path()` handlers, `layout()` handlers, `parallel()` slots,
39
+ `intercept()` routes, and loader registrations:
38
40
 
39
41
  ```typescript
40
42
  // urls.tsx — path/layout/parallel/intercept/loader/revalidate are passed in by urls()
41
43
  import { urls } from "@rangojs/router";
44
+ import * as CartActions from "./actions/cart";
42
45
 
43
46
  export const urlpatterns = urls(({ path, loader, revalidate }) => [
44
47
  // The loader belongs to the route that consumes its data — nest it inside
45
48
  // the owning path() so the segment owns its data dependency.
46
49
  path("/cart", CartPage, { name: "cart" }, () => [
47
- revalidate(
48
- ({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
49
- ),
50
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
50
51
  loader(CartLoader, () => [
51
- revalidate(
52
- ({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
53
- ),
52
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
54
53
  ]),
55
54
  ]),
56
55
  ]);
57
56
  ```
58
57
 
59
- For module-level `"use server"` files, the `actionId` passed to every
58
+ `ctx.isAction()` resolves the imported action reference the same way the router
59
+ derives `actionId`, so it matches in both dev and production and survives action
60
+ renames/moves as type errors instead of silent substring drift.
61
+
62
+ For module-level `"use server"` files, the raw `actionId` passed to every
60
63
  server-side `revalidate()` predicate is path-bearing in the server/RSC
61
- environment in both dev and production: `src/actions/cart.ts#addToCart`. This
62
- is intentional so path, layout, parallel, intercept, and loader revalidation
63
- predicates can filter by action file, directory, or export name.
64
+ environment in both dev and production: `src/actions/cart.ts#addToCart`. This is
65
+ the escape hatch for broad filters by action file, directory, or export name.
64
66
 
65
67
  Actions and the follow-up revalidation render share one request context.
66
68
  Values written in the action with `ctx.set(MyVar, value)` or `ctx.set("key",
@@ -91,15 +93,18 @@ export async function switchTenant(tenantId: string) {
91
93
  ```typescript
92
94
  // urls.tsx
93
95
  import { urls } from "@rangojs/router";
96
+ import * as TenantActions from "./actions/tenant";
94
97
  import { ChangedTenant } from "./context";
95
98
 
96
99
  export const urlpatterns = urls(({ path, revalidate }) => [
97
100
  path("/dashboard/:tenantId", DashboardPage, { name: "dashboard" }, () => [
98
- revalidate(
99
- ({ actionId, context }) =>
100
- actionId?.startsWith("src/actions/tenant.ts#") &&
101
- context.get(ChangedTenant) === context.params.tenantId,
102
- ),
101
+ revalidate((ctx) => {
102
+ if (!ctx.isAction(TenantActions)) return undefined;
103
+ return (
104
+ ctx.context.get(ChangedTenant) === ctx.context.params.tenantId ||
105
+ undefined
106
+ );
107
+ }),
103
108
  ]),
104
109
  ]);
105
110
  ```
@@ -380,13 +385,18 @@ re-render so the UI updates. Rango runs the action, then evaluates
380
385
  `revalidate()` on matched segments and loaders. Each path, layout, parallel,
381
386
  intercept, or loader rule decides whether that piece re-renders/re-resolves.
382
387
 
383
- The `actionId` arrives as part of the revalidation context match it to
384
- scope re-runs to specific actions.
388
+ Use `ctx.isAction()` for specific actions or modules. It accepts one action,
389
+ several actions, or a namespace import (`import * as CartActions`). Pair it with
390
+ `|| undefined` for "revalidate on match, otherwise defer to defaults/downstream
391
+ rules."
385
392
 
386
393
  ```typescript
387
394
  // urls.tsx — inside the urls() callback. Nest each loader inside the path(),
388
395
  // layout(), or parallel() that owns its data so the route tree mirrors the
389
396
  // data dependencies.
397
+ import * as AccountActions from "./actions/account";
398
+ import * as CartActions from "./actions/cart";
399
+
390
400
  urls(({ path, loader, revalidate }) => [
391
401
  path("/", HomePage, { name: "home" }, () => [
392
402
  // Loader data re-runs by default after any action. Opt out with revalidate(() => false).
@@ -395,36 +405,37 @@ urls(({ path, loader, revalidate }) => [
395
405
 
396
406
  // Re-render the cart page handler AND re-resolve its loader after cart actions
397
407
  path("/cart", CartPage, { name: "cart" }, () => [
398
- revalidate(
399
- ({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
400
- ),
408
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
401
409
  loader(CartLoader, () => [
402
- revalidate(
403
- ({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
404
- ),
410
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
405
411
  ]),
406
412
  ]),
407
413
 
408
- // Re-run after any action under src/actions/account/
414
+ // Re-run after any action exported by the account actions module
409
415
  path("/account", AccountPage, { name: "account" }, () => [
410
416
  loader(AccountLoader, () => [
411
- revalidate(
412
- ({ actionId }) => actionId?.startsWith("src/actions/account/") ?? false,
413
- ),
417
+ revalidate((ctx) => ctx.isAction(AccountActions) || undefined),
414
418
  ]),
415
419
  ]),
416
420
  ]);
417
421
  ```
418
422
 
419
- `actionId` is stable per action. For actions exported from a module-level
420
- `"use server"` file, the ID is prefixed with the source file path
421
- (`src/actions/cart.ts#addToCart`), so substring matching by file path is the
422
- recommended scope. **Inline `"use server"` actions** (declared inside an RSC
423
- component) intentionally keep their hashed IDs — file paths are withheld
424
- from the client for security. If you need file-path-based revalidation
425
- predicates, define the action in a module-level `"use server"` file rather
426
- than inline. See `/loader` for the full revalidation contract (deferred
427
- returns, soft suggestions).
423
+ The raw `actionId` string stays available for broad path filters:
424
+
425
+ ```typescript
426
+ // Match any action under src/actions/account/, including modules not imported here.
427
+ revalidate(
428
+ ({ actionId }) => actionId?.startsWith("src/actions/account/") || undefined,
429
+ );
430
+ ```
431
+
432
+ For actions exported from a module-level `"use server"` file, the ID is prefixed
433
+ with the source file path (`src/actions/cart.ts#addToCart`). **Inline `"use
434
+ server"` actions** (declared inside an RSC component) intentionally keep their
435
+ hashed IDs — file paths are withheld from the client for security. If you need
436
+ file-path-based revalidation predicates, define the action in a module-level
437
+ `"use server"` file rather than inline. See `/loader` for the full revalidation
438
+ contract (deferred returns, soft suggestions).
428
439
 
429
440
  ### Cross-segment dependencies
430
441
 
@@ -434,8 +445,9 @@ stale context. Share the same `revalidate` predicate on both producer and
434
445
  consumer:
435
446
 
436
447
  ```typescript
437
- const revalidateCart = ({ actionId }) =>
438
- actionId?.startsWith("src/actions/cart.ts#") ?? false;
448
+ import * as CartActions from "./actions/cart";
449
+
450
+ const revalidateCart = (ctx) => ctx.isAction(CartActions) || undefined;
439
451
 
440
452
  urls(({ path, layout, loader, revalidate }) => [
441
453
  layout(CartLayout, () => [
@@ -0,0 +1,128 @@
1
+ ---
2
+ name: testing
3
+ description: Test @rangojs/router apps — unit (loaders/middleware/reverse/components), integration (dispatch/Flight), and e2e (dev+prod parity, progressive enhancement)
4
+ argument-hint: [layer]
5
+ ---
6
+
7
+ # Testing @rangojs/router apps
8
+
9
+ Rango ships six consumer-facing testing entries, one per test runtime/dependency:
10
+ `@rangojs/router/testing` (unit + integration, under a Vite-driven Vitest
11
+ project), `@rangojs/router/testing/vitest` (the `rangoTestConfig`/`rangoTestAliases`
12
+ setup preset), `@rangojs/router/testing/dom` (`renderRoute`, needs RTL + a DOM
13
+ env), `@rangojs/router/testing/e2e` (the Playwright harness),
14
+ `@rangojs/router/testing/flight` (real Flight, react-server condition only), and
15
+ `@rangojs/router/testing/flight-matchers` (the Flight matchers).
16
+
17
+ The hard problem in an RSC app is that the layer you reach for is dictated by
18
+ **what the behavior touches** — a pure predicate is a one-line vitest test; a real
19
+ async Server Component cannot be a plain node test at all. Pick the layer
20
+ **first**, then the primitive. Reaching one layer too high (e2e for a reverse
21
+ function) is slow; one too low (a node test for Flight) fails to compile or
22
+ silently asserts nothing.
23
+
24
+ This page is the router. Each primitive's full API (options, the seeded context
25
+ your code receives, the return shape), a minimal recipe, and its caveats live in a
26
+ dedicated sub-file linked from the decision tree below. Read the one for your case.
27
+
28
+ > **Setup is the first wall.** The vitest projects, the `rangoTestConfig` vs
29
+ > `rangoTestAliases` choice (Node >= 23), and the react-server `@rangojs/router ->
30
+ index.rsc.ts` alias are all in [`./setup.md`](./setup.md). Read it before writing
31
+ > `vitest.config.ts`. Platform bindings (`env.DB`/DO/R2) are your own double —
32
+ > [`./bindings.md`](./bindings.md).
33
+
34
+ For the long-form prose guide (setup walkthrough + migration), see
35
+ [`docs/testing.md`](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md)
36
+ (the `docs/` directory is not shipped in the published package, so this is an
37
+ absolute link).
38
+
39
+ ## When to use
40
+
41
+ Use this skill when adding or changing tests for a Rango app: a loader,
42
+ middleware, a server action, a route map, a client component, a response route,
43
+ cache/SWR behavior, prerender, or a navigation/PE flow.
44
+
45
+ Two non-negotiable mandates (from the repo's `CLAUDE.md`, and they apply to
46
+ consumer apps too):
47
+
48
+ - **Every e2e covers BOTH dev and production.** A dev-only e2e is not acceptable.
49
+ Use `parityDescribe` — it generates the dev and production describes from one
50
+ body, so you cannot forget the prod half. See [`./e2e-parity.md`](./e2e-parity.md).
51
+ - **Progressive-enhancement parity** is a first-class assertion. A form-driven
52
+ flow must produce the same observable result with JS on and JS off. Use
53
+ `expectParity`.
54
+
55
+ ## The read-first shape
56
+
57
+ Four import roots, each matched to the dependency/runtime that can load it — this
58
+ split is forced by hard walls, not preference:
59
+
60
+ - `@rangojs/router/testing` — unit + integration primitives. Run these under a
61
+ **Vite-driven Vitest** project with the rango Vite plugin active (the router
62
+ internals import the `@rangojs/router:version` virtual; without the plugin, the
63
+ preset stubs it). References neither React, RTL, Playwright, nor the RSC runtime.
64
+ - `@rangojs/router/testing/dom` — `renderRoute` (the RTL component stub). Kept
65
+ separate so the unit barrel stays free of React/RTL; it lazy-loads
66
+ `@testing-library/react` and needs a DOM env (happy-dom/jsdom).
67
+ - `@rangojs/router/testing/e2e` — the Playwright harness. Kept separate so it
68
+ loads in a plain (non-Vite) Playwright runner; the helpers take your
69
+ `test`/`expect`, so this entry never imports `@playwright/test` at runtime.
70
+ - `@rangojs/router/testing/flight` — real Flight rendering. Its serializer loads
71
+ only under the `react-server` node condition; pulling it elsewhere throws.
72
+
73
+ The single rule that drives everything:
74
+
75
+ > **If the behavior needs a real Flight render, it cannot be a plain vitest node
76
+ > test.** It is either `renderToFlightString`/`renderServerTree`/`renderHandler`
77
+ > (under the react-server vitest project) or an e2e test. There is no middle
78
+ > ground in node.
79
+
80
+ ## Decision tree: behavior -> layer -> primitive
81
+
82
+ Each primitive links to its sub-file (API + recipe + caveats).
83
+
84
+ | The behavior is… | Layer | Primitive | Import root |
85
+ | --------------------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------ | -------------------------------- |
86
+ | a pure function / `reverse` / `href` / a predicate (`revalidate`, `isAction`) | unit + types | [`reverse`/`@ts-expect-error`](./reverse-and-types.md) | `@rangojs/router/testing` |
87
+ | one loader's data logic | unit (node) | [`runLoader`](./loader.md) | `@rangojs/router/testing` |
88
+ | one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md) | `@rangojs/router/testing` |
89
+ | a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md) | `@rangojs/router/testing` |
90
+ | a handle's `collect`/accumulator, or a seeded handle read | unit | [`collectHandle` / seeded `handles`](./handles.md) | `@rangojs/router/testing[/dom]` |
91
+ | a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
92
+ | a redirect / status / headers / cookies / **response route** (json/text/html/xml/md), no Flight | integration | [`dispatch`](./response-routes.md) | `@rangojs/router/testing` |
93
+ | a real async **Server Component** / Flight serialization shape | RSC unit | [`renderToFlightString` + `toMatchFlight`](./flight.md) | `@rangojs/router/testing/flight` |
94
+ | a client island's **typed props** / the **server-rendered** host content | RSC unit | [`renderServerTree` + `findClientBoundaries`/`findElements`](./server-tree.md) | `@rangojs/router/testing/flight` |
95
+ | a real route **handler** `(ctx) => rsc` (params/loaders/vars -> rendered RSC + effects) | RSC unit | [`renderHandler`](./render-handler.md) | `@rangojs/router/testing/flight` |
96
+ | navigation, hydration, PE parity, view transitions, real SSR | e2e | [`createRangoE2E` -> `parityDescribe`/`expectParity`](./e2e-parity.md) | `@rangojs/router/testing/e2e` |
97
+ | cache hit/miss/stale, prerender (= a cache hit by design) | e2e + signal | [`assertCacheStatus` / telemetry sink](./cache-prerender.md) | `@rangojs/router/testing[/e2e]` |
98
+ | generated route map drift vs runtime | unit (node) | [`assertGeneratedRoutesMatch`](./reverse-and-types.md) | `@rangojs/router/testing` |
99
+ | a platform binding (`env.DB` / Durable Object / `env.R2`) | unit/integr. | [your own double via `env`](./bindings.md) | (any primitive's `env` option) |
100
+
101
+ Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
102
+ `/handler-use`, `/hooks`, `/response-routes`, `/route`, `/caching`, `/prerender`,
103
+ `/typesafety`.
104
+
105
+ ## Sub-files
106
+
107
+ - Cross-cutting: [`setup.md`](./setup.md), [`bindings.md`](./bindings.md)
108
+ - Unit (node): [`loader.md`](./loader.md), [`middleware.md`](./middleware.md),
109
+ [`server-actions.md`](./server-actions.md), [`handles.md`](./handles.md),
110
+ [`reverse-and-types.md`](./reverse-and-types.md)
111
+ - Unit (DOM): [`client-components.md`](./client-components.md)
112
+ - RSC unit: [`flight.md`](./flight.md), [`server-tree.md`](./server-tree.md),
113
+ [`render-handler.md`](./render-handler.md)
114
+ - Integration: [`response-routes.md`](./response-routes.md)
115
+ - E2E: [`e2e-parity.md`](./e2e-parity.md), [`cache-prerender.md`](./cache-prerender.md)
116
+
117
+ ## Pre-push checklist (mirror CLAUDE.md)
118
+
119
+ Before pushing, run all of these and fix any failure:
120
+
121
+ 1. `pnpm run typecheck` (or `pnpm exec tsc --noEmit`)
122
+ 2. `pnpm run test:unit` (node + DOM vitest)
123
+ 3. `pnpm run test:unit:rsc` (the react-server Flight project)
124
+ 4. `pnpm run lint`
125
+ 5. `pnpm run format`
126
+
127
+ And: **every e2e has a production counterpart.** `parityDescribe` makes this
128
+ automatic — if you wrote a plain `test.describe` for a behavior, convert it.