@rangojs/router 0.16.0 → 0.18.0

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 (309) hide show
  1. package/README.md +255 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/cookie-name.d.ts +6 -0
  4. package/dist/types/browser/history-state.d.ts +16 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  6. package/dist/types/browser/partial-update.d.ts +11 -0
  7. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  8. package/dist/types/browser/rsc-router.d.ts +1 -1
  9. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  10. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  11. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  12. package/dist/types/cache/cache-policy.d.ts +17 -0
  13. package/dist/types/cache/cache-scope.d.ts +132 -19
  14. package/dist/types/cache/cache-tag.d.ts +92 -7
  15. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  18. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  19. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  20. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  21. package/dist/types/cache/handle-capture.d.ts +41 -3
  22. package/dist/types/cache/handle-snapshot.d.ts +54 -13
  23. package/dist/types/cache/index.d.ts +1 -0
  24. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  25. package/dist/types/cache/segment-codec.d.ts +10 -3
  26. package/dist/types/cache/shell-memo.d.ts +178 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  28. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  29. package/dist/types/cache/taint.d.ts +1 -1
  30. package/dist/types/cache/types.d.ts +220 -44
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  32. package/dist/types/client.d.ts +4 -4
  33. package/dist/types/client.rsc.d.ts +4 -5
  34. package/dist/types/components/DefaultDocument.d.ts +5 -2
  35. package/dist/types/context-var.d.ts +6 -5
  36. package/dist/types/escape-script.d.ts +1 -1
  37. package/dist/types/handles/MetaTags.d.ts +5 -4
  38. package/dist/types/handles/Scripts.d.ts +8 -7
  39. package/dist/types/handles/meta.d.ts +2 -2
  40. package/dist/types/handles/script.d.ts +17 -16
  41. package/dist/types/href-client.d.ts +1 -1
  42. package/dist/types/html.d.ts +12 -0
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +3 -2
  45. package/dist/types/loader-store.d.ts +7 -4
  46. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  47. package/dist/types/reverse.d.ts +36 -4
  48. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  49. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  50. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  51. package/dist/types/route-types.d.ts +1 -1
  52. package/dist/types/router/debug-manifest.d.ts +3 -2
  53. package/dist/types/router/error-handling.d.ts +20 -0
  54. package/dist/types/router/handler-context.d.ts +6 -0
  55. package/dist/types/router/instrument.d.ts +8 -0
  56. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  57. package/dist/types/router/middleware-types.d.ts +9 -3
  58. package/dist/types/router/router-context.d.ts +3 -1
  59. package/dist/types/router/router-interfaces.d.ts +24 -2
  60. package/dist/types/router/router-options.d.ts +8 -5
  61. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  62. package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
  63. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  64. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
  65. package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  67. package/dist/types/router/segment-wrappers.d.ts +1 -1
  68. package/dist/types/rsc/full-payload.d.ts +14 -0
  69. package/dist/types/rsc/handler.d.ts +1 -1
  70. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  71. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  72. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
  74. package/dist/types/rsc/shell-capture.d.ts +151 -19
  75. package/dist/types/rsc/shell-serve.d.ts +140 -17
  76. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  77. package/dist/types/rsc/types.d.ts +17 -0
  78. package/dist/types/server/context.d.ts +139 -15
  79. package/dist/types/server/cookie-store.d.ts +21 -0
  80. package/dist/types/server/handle-store.d.ts +26 -2
  81. package/dist/types/server/request-context.d.ts +182 -53
  82. package/dist/types/ssr/index.d.ts +24 -0
  83. package/dist/types/ssr/ssr-root.d.ts +9 -8
  84. package/dist/types/static-handler.d.ts +4 -1
  85. package/dist/types/testing/flight.entry.d.ts +7 -0
  86. package/dist/types/testing/generated-routes.d.ts +2 -2
  87. package/dist/types/testing/internal/context.d.ts +1 -1
  88. package/dist/types/testing/render-route.d.ts +32 -6
  89. package/dist/types/testing/run-loader.d.ts +18 -3
  90. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  91. package/dist/types/testing/shell-status.d.ts +23 -10
  92. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  93. package/dist/types/testing/vitest.d.ts +35 -1
  94. package/dist/types/theme/ThemeScript.d.ts +3 -3
  95. package/dist/types/types/cache-types.d.ts +14 -10
  96. package/dist/types/types/handler-context.d.ts +28 -1
  97. package/dist/types/types/loader-types.d.ts +8 -6
  98. package/dist/types/urls/pattern-types.d.ts +22 -18
  99. package/dist/types/urls/response-types.d.ts +3 -12
  100. package/dist/types/use-loader.d.ts +12 -3
  101. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  102. package/dist/types/vite/rango.d.ts +1 -1
  103. package/dist/types/vite/router-discovery.d.ts +29 -2
  104. package/dist/vite/index.js +1420 -1348
  105. package/package.json +2 -2
  106. package/skills/api-client/SKILL.md +22 -10
  107. package/skills/breadcrumbs/SKILL.md +75 -22
  108. package/skills/bundle-analysis/SKILL.md +22 -22
  109. package/skills/cache-guide/SKILL.md +306 -124
  110. package/skills/caching/SKILL.md +530 -98
  111. package/skills/catalog.json +16 -16
  112. package/skills/client-urls/SKILL.md +65 -38
  113. package/skills/cloudflare/SKILL.md +87 -20
  114. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  115. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  116. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  117. package/skills/comparison/SKILL.md +5 -0
  118. package/skills/comparison/references/framework-comparison.md +75 -57
  119. package/skills/composability/SKILL.md +38 -29
  120. package/skills/css/SKILL.md +10 -13
  121. package/skills/debug-manifest/SKILL.md +74 -31
  122. package/skills/defer-hydration/SKILL.md +52 -31
  123. package/skills/deployment-caching/SKILL.md +11 -3
  124. package/skills/document-cache/SKILL.md +98 -28
  125. package/skills/fonts/SKILL.md +16 -8
  126. package/skills/handler-use/SKILL.md +55 -17
  127. package/skills/hooks/SKILL.md +28 -20
  128. package/skills/hooks/data.md +23 -3
  129. package/skills/hooks/handle-and-actions.md +34 -17
  130. package/skills/hooks/navigation.md +39 -10
  131. package/skills/hooks/outlets.md +13 -6
  132. package/skills/hooks/state.md +39 -15
  133. package/skills/hooks/urls.md +7 -5
  134. package/skills/host-router/SKILL.md +64 -12
  135. package/skills/i18n/SKILL.md +60 -34
  136. package/skills/intercept/SKILL.md +161 -26
  137. package/skills/layout/SKILL.md +106 -50
  138. package/skills/links/SKILL.md +51 -19
  139. package/skills/loader/SKILL.md +386 -128
  140. package/skills/middleware/SKILL.md +129 -26
  141. package/skills/migrate-nextjs/SKILL.md +156 -70
  142. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  143. package/skills/migrate-react-router/SKILL.md +51 -39
  144. package/skills/migrate-react-router/component-migration.md +28 -7
  145. package/skills/migrate-react-router/data-and-actions.md +21 -6
  146. package/skills/migrate-react-router/route-mapping.md +19 -10
  147. package/skills/mime-routes/SKILL.md +12 -7
  148. package/skills/observability/SKILL.md +92 -14
  149. package/skills/parallel/SKILL.md +88 -47
  150. package/skills/ppr/SKILL.md +866 -379
  151. package/skills/prerender/SKILL.md +210 -108
  152. package/skills/rango/SKILL.md +115 -65
  153. package/skills/react-compiler/SKILL.md +6 -5
  154. package/skills/response-routes/SKILL.md +133 -25
  155. package/skills/route/SKILL.md +170 -60
  156. package/skills/router-setup/SKILL.md +420 -240
  157. package/skills/scripts/SKILL.md +39 -21
  158. package/skills/server-actions/SKILL.md +231 -59
  159. package/skills/shell-manifest/SKILL.md +28 -14
  160. package/skills/streams-and-websockets/SKILL.md +32 -14
  161. package/skills/tailwind/SKILL.md +37 -33
  162. package/skills/testing/SKILL.md +73 -33
  163. package/skills/testing/cache-prerender.md +60 -33
  164. package/skills/testing/client-components.md +59 -6
  165. package/skills/testing/e2e-parity.md +12 -2
  166. package/skills/testing/flight.md +19 -11
  167. package/skills/testing/handles.md +8 -5
  168. package/skills/testing/loader.md +26 -6
  169. package/skills/testing/middleware.md +8 -3
  170. package/skills/testing/predicates.md +120 -0
  171. package/skills/testing/render-handler.md +70 -22
  172. package/skills/testing/response-routes.md +11 -4
  173. package/skills/testing/reverse-and-types.md +20 -14
  174. package/skills/testing/serve-shell-request.md +188 -0
  175. package/skills/testing/server-actions.md +12 -3
  176. package/skills/testing/server-tree.md +9 -0
  177. package/skills/testing/setup.md +9 -5
  178. package/skills/theme/SKILL.md +155 -30
  179. package/skills/typesafety/SKILL.md +20 -9
  180. package/skills/typesafety/env-and-bindings.md +137 -50
  181. package/skills/typesafety/generated-files-and-cli.md +26 -14
  182. package/skills/typesafety/params-and-search.md +44 -19
  183. package/skills/typesafety/route-types.md +50 -14
  184. package/skills/use-cache/SKILL.md +224 -83
  185. package/skills/vercel/SKILL.md +35 -13
  186. package/skills/view-transitions/SKILL.md +42 -21
  187. package/src/__augment-tests__/augmented.check.ts +26 -1
  188. package/src/browser/cookie-name.ts +23 -0
  189. package/src/browser/history-state.ts +23 -0
  190. package/src/browser/index.ts +2 -2
  191. package/src/browser/navigation-bridge.ts +3 -4
  192. package/src/browser/navigation-transaction.ts +22 -2
  193. package/src/browser/partial-update.ts +35 -25
  194. package/src/browser/react/ScrollRestoration.tsx +6 -6
  195. package/src/browser/rsc-router.tsx +6 -1
  196. package/src/browser/scroll-restoration.ts +53 -6
  197. package/src/cache/cache-exec-scope.ts +37 -4
  198. package/src/cache/cache-key-utils.ts +23 -8
  199. package/src/cache/cache-policy.ts +22 -0
  200. package/src/cache/cache-runtime.ts +264 -68
  201. package/src/cache/cache-scope.ts +419 -43
  202. package/src/cache/cache-tag.ts +375 -11
  203. package/src/cache/cf/cf-base64.ts +36 -4
  204. package/src/cache/cf/cf-cache-constants.ts +12 -0
  205. package/src/cache/cf/cf-cache-store.ts +990 -348
  206. package/src/cache/cf/cf-cache-types.ts +32 -4
  207. package/src/cache/cf/cf-shell-frame.ts +258 -0
  208. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  209. package/src/cache/document-cache.ts +15 -0
  210. package/src/cache/handle-capture.ts +90 -10
  211. package/src/cache/handle-snapshot.ts +133 -28
  212. package/src/cache/index.ts +2 -0
  213. package/src/cache/isolate-tag-memo.ts +308 -0
  214. package/src/cache/segment-codec.ts +26 -7
  215. package/src/cache/shell-memo.ts +341 -0
  216. package/src/cache/shell-snapshot.ts +217 -27
  217. package/src/cache/tag-invalidation.ts +108 -22
  218. package/src/cache/taint.ts +1 -1
  219. package/src/cache/types.ts +225 -44
  220. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  221. package/src/client.rsc.tsx +10 -9
  222. package/src/client.tsx +5 -4
  223. package/src/components/DefaultDocument.tsx +7 -2
  224. package/src/context-var.ts +6 -5
  225. package/src/escape-script.ts +1 -1
  226. package/src/handles/MetaTags.tsx +13 -11
  227. package/src/handles/Scripts.tsx +11 -10
  228. package/src/handles/meta.ts +2 -2
  229. package/src/handles/script.ts +17 -16
  230. package/src/href-client.ts +1 -1
  231. package/src/html.ts +12 -0
  232. package/src/index.rsc.ts +5 -0
  233. package/src/index.ts +1 -0
  234. package/src/loader-store.ts +7 -4
  235. package/src/prerender/build-shell-capture.ts +73 -22
  236. package/src/reverse.ts +44 -4
  237. package/src/route-definition/dsl-helpers.ts +121 -33
  238. package/src/route-definition/helpers-types.ts +33 -9
  239. package/src/route-definition/resolve-handler-use.ts +121 -6
  240. package/src/route-types.ts +0 -3
  241. package/src/router/debug-manifest.ts +10 -2
  242. package/src/router/error-handling.ts +51 -15
  243. package/src/router/handler-context.ts +71 -28
  244. package/src/router/instrument.ts +54 -2
  245. package/src/router/intercept-resolution.ts +141 -43
  246. package/src/router/loader-resolution.ts +115 -39
  247. package/src/router/match-api.ts +40 -26
  248. package/src/router/match-handlers.ts +5 -0
  249. package/src/router/match-middleware/background-revalidation.ts +161 -96
  250. package/src/router/match-middleware/cache-lookup.ts +110 -10
  251. package/src/router/match-middleware/cache-store.ts +48 -101
  252. package/src/router/middleware-types.ts +9 -6
  253. package/src/router/middleware.ts +75 -13
  254. package/src/router/prerender-match.ts +59 -19
  255. package/src/router/route-snapshot.ts +11 -1
  256. package/src/router/router-context.ts +2 -0
  257. package/src/router/router-interfaces.ts +25 -2
  258. package/src/router/router-options.ts +8 -5
  259. package/src/router/segment-resolution/fresh.ts +62 -21
  260. package/src/router/segment-resolution/helpers.ts +4 -2
  261. package/src/router/segment-resolution/loader-cache.ts +407 -76
  262. package/src/router/segment-resolution/loader-mask.ts +12 -22
  263. package/src/router/segment-resolution/loader-snapshot.ts +208 -56
  264. package/src/router/segment-resolution/mask-nested.ts +138 -44
  265. package/src/router/segment-resolution/revalidation.ts +61 -16
  266. package/src/router/segment-wrappers.ts +4 -0
  267. package/src/rsc/full-payload.ts +25 -1
  268. package/src/rsc/handler.ts +14 -2
  269. package/src/rsc/progressive-enhancement.ts +3 -2
  270. package/src/rsc/render-pipeline.ts +3 -0
  271. package/src/rsc/response-cache-serve.ts +9 -6
  272. package/src/rsc/response-route-handler.ts +11 -3
  273. package/src/rsc/rsc-rendering.ts +608 -225
  274. package/src/rsc/shell-build-manifest.ts +27 -9
  275. package/src/rsc/shell-capture-constants.ts +73 -6
  276. package/src/rsc/shell-capture.ts +1068 -296
  277. package/src/rsc/shell-serve.ts +316 -34
  278. package/src/rsc/ssr-setup.ts +14 -8
  279. package/src/rsc/types.ts +18 -0
  280. package/src/server/context.ts +300 -32
  281. package/src/server/cookie-store.ts +87 -61
  282. package/src/server/handle-store.ts +232 -7
  283. package/src/server/request-context.ts +297 -67
  284. package/src/ssr/index.tsx +136 -44
  285. package/src/ssr/ssr-root.tsx +9 -8
  286. package/src/static-handler.ts +4 -1
  287. package/src/testing/flight-runtime.d.ts +21 -0
  288. package/src/testing/flight.entry.ts +14 -0
  289. package/src/testing/generated-routes.ts +2 -2
  290. package/src/testing/internal/context.ts +1 -1
  291. package/src/testing/internal/flight-client-globals.ts +7 -5
  292. package/src/testing/render-route.tsx +85 -24
  293. package/src/testing/run-loader.ts +68 -6
  294. package/src/testing/serve-shell-request.ts +378 -0
  295. package/src/testing/shell-status.ts +34 -12
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  297. package/src/testing/vitest.ts +62 -1
  298. package/src/theme/ThemeProvider.tsx +7 -6
  299. package/src/theme/ThemeScript.tsx +3 -3
  300. package/src/types/cache-types.ts +14 -10
  301. package/src/types/handler-context.ts +28 -1
  302. package/src/types/loader-types.ts +8 -6
  303. package/src/urls/pattern-types.ts +22 -18
  304. package/src/urls/response-types.ts +3 -23
  305. package/src/use-loader.tsx +12 -3
  306. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  307. package/src/vite/plugins/use-cache-transform.ts +118 -74
  308. package/src/vite/rango.ts +7 -3
  309. package/src/vite/router-discovery.ts +14 -4
@@ -26,14 +26,16 @@ with the shape, then pick a primitive.
26
26
  inside a cached render. They run **in parallel** right after middleware and
27
27
  **stream**, so data latency overlaps first paint instead of blocking it (a
28
28
  cache hit streams UI instantly while loaders resolve fresh alongside). Opt into
29
- caching explicitly. See `/loader` → "Parallel and streaming".
29
+ caching explicitly. See `/loader` → "Parallel and streaming". The one
30
+ exception is a `ppr` route's `ssr: false` loader: it bakes into the shell
31
+ and runs on a shell HIT only when its return carries promises (`/ppr`).
30
32
  - **One identity, one store** — loaders, handles, cached fns, and actions are all
31
33
  `path#export`; all caches share one store. Entries expire by TTL/SWR, and are
32
34
  tagged via `cache({ tags })` or runtime `cacheTag(...tags)`; built-in stores
33
35
  index by tag and invalidate via `updateTag(...tags)` (awaitable, read-your-own-writes)
34
36
  or `revalidateTag(...tags)` (background, non-blocking).
35
37
  - **Type-safe end to end** — route names, params, search schemas, loader return
36
- types, context vars, and `href` / `reverse` are checked at compile time
38
+ types, context vars, and `href()` / `ctx.reverse()` are checked at compile time
37
39
  (`/typesafety`).
38
40
  - **See where time goes** — turn on `debugPerformance` early (router option, or
39
41
  `ctx.debugPerformance()` in middleware for per-request opt-in). It prints a
@@ -42,7 +44,9 @@ with the shape, then pick a primitive.
42
44
  console, OpenTelemetry, or custom sink. See `/observability`.
43
45
 
44
46
  Most features are **just-in-time**: the core is `urls()`, `path()`, `layout()`,
45
- `include()`, and `reverse()`. Caching, parallel routes, intercepts, prerender,
47
+ `include()`, and URL generation (`ctx.reverse()` on the server, `href()` /
48
+ `useReverse()` on the client; there is no standalone `reverse()` export — see
49
+ `/links`). Caching, parallel routes, intercepts, prerender,
46
50
  i18n, themes, and the rest are opt-in — reach for them when a requirement
47
51
  appears, not up front.
48
52
 
@@ -66,8 +70,9 @@ Reach for the next rung only when the one above doesn't fit — the higher rungs
66
70
  are immune to partial-revalidation staleness by construction.
67
71
 
68
72
  1. **A loader** (`loader()` + `useLoader()`). Loaders resolve fresh on every
69
- pass — full renders, action revalidations, cache hits. Nothing to keep in
70
- sync. If the data can be a loader, make it a loader.
73
+ pass — full renders, action revalidations, cache hits, PPR shell HITs (on a
74
+ `ppr` route, keep it off `ssr: false`, which bakes it into the shell).
75
+ Nothing to keep in sync. If the data can be a loader, make it a loader.
71
76
  2. **Middleware `ctx.set()`**. Route middleware wraps every render pass,
72
77
  including post-action revalidation and PE re-renders, so its variables are
73
78
  never stale. Right for request-shaped context: auth, session, locale.
@@ -112,7 +117,7 @@ stated, greppable contract.
112
117
  | guarantee loader output in the SSR HTML | `loader(L, { ssr: false })` | /loader |
113
118
  | pre-render a route at build time | `Prerender(...)` wrapper | /prerender |
114
119
  | feed live loaders from a cached shell | replayed handle + `ctx.rendered()` | /shell-manifest |
115
- | cache the HTML shell, keep loaders live | `ppr` path option | /ppr |
120
+ | cache HTML shell, live loaders as holes | `ppr` path option | /ppr |
116
121
  | choose in-function vs CDN caching | deployment cache boundary | /deployment-caching |
117
122
  | stream SSE / upgrade a WebSocket | `path.stream()` / `path.any()` | /streams-and-websockets |
118
123
 
@@ -121,19 +126,30 @@ stated, greppable contract.
121
126
  - `path()`/`include()` are always visible in `urls()`; config helpers are extractable.
122
127
  - **Cache decides freshness; `revalidate()` decides client-update.** Orthogonal; compose.
123
128
  - Loaders resolve fresh every request (even inside `cache()`) and never run twice/request.
129
+ The exception is a `ppr` route's `ssr: false` loader: it bakes into the shell
130
+ and runs on a shell HIT only when its return carries promises.
124
131
  - **The consumption-lane rule.** For every shared artifact (`cache()`,
125
132
  `"use cache"`, the PPR shell): server-side handler consumption
126
- (`await ctx.use(loader)`) yields a BAKED copy — identity reads
127
- (`cookies()`/`headers()`) are permitted there and the capture-time value
128
- freezes into the shared artifact (a documented footgun; see `/caching` →
129
- "Cache purity & tainted objects"). Client-side consumption (`useLoader` in
130
- a `"use client"` component) is the LIVE lane. DSL `loader()` segments
131
- follow their lane machinery (live under renderable `loading()`, bake
132
- otherwise). Pinned by semantic-matrix row PPR3.
133
+ (`await ctx.use(loader)`) yields a BAKED copy — the capture-time value
134
+ freezes into the shared artifact. Identity reads (`cookies()`/`headers()`,
135
+ a `{ cache: false }` variable) inside that loader are permitted under
136
+ `cache()`/`"use cache"` (a documented footgun; see `/caching` → "Cache
137
+ purity & tainted objects") but REFUSE a PPR capture: the route serves
138
+ uncached until the read moves to a live loader. Client-side consumption
139
+ (`useLoader` in a `"use client"` component) is the LIVE lane. DSL
140
+ `loader()` segments follow their PPR lane (only `ssr: false` bakes; see
141
+ `/ppr` → The loader lane rule).
142
+ - **A PPR shell HIT never runs a handler.** Everything a handler produces
143
+ (promises it passes under `<Suspense>`, async server components, handle
144
+ pushes, loaders it awaits) is shell material, as under `cache()`; a HIT
145
+ runs middleware and live loaders only (an `ssr: false` loader runs there
146
+ only when its return carries promises). Live data belongs in a loader without
147
+ `ssr: false`, read with `useLoader` under `loading()` or an inline
148
+ `<Suspense>`.
133
149
  - Inside `"use cache"`: `cookies()`/`headers()` and `ctx` side-effects
134
150
  (`set`/`header`/`setTheme`/`onResponse`/`setLocationState`) throw; `ctx.use(Handle)`
135
- is captured on miss and replayed on hit. (The non-cacheable read guard is a
136
- separate `cache()`-boundary check — see the correctness bullet below.)
151
+ is captured on miss and replayed on hit. A non-cacheable variable read
152
+ (`createVar({ cache: false })`) throws too — see the correctness bullet below.
137
153
  - One identity `path#export` (`functionId`/`$$id`/`actionId`); one store. Freshness
138
154
  is TTL/SWR expiry plus tag-based invalidation: tag via `cache({ tags })` /
139
155
  `cacheTag(...tags)`, then `updateTag(...tags)` (awaitable) or `revalidateTag(...tags)`
@@ -142,7 +158,7 @@ stated, greppable contract.
142
158
  - Caches are correctness-first: persistent store keys are version-segmented (no
143
159
  cross-deploy drift), the forward/back cache is mutation-aware, and
144
160
  `createVar({ cache: false })` throws on a **direct** read inside a `cache()`
145
- boundary (a deliberately non-propagating guard). See `/cache-guide` →
161
+ boundary or a `"use cache"` function (a deliberately non-propagating guard). See `/cache-guide` →
146
162
  "Correctness & invalidation".
147
163
  - Nested caches: the outer cache window bounds the inner — an inner shorter TTL
148
164
  only applies when the enclosing cache recomputes; put a value in a loader if it
@@ -160,17 +176,22 @@ stated, greppable contract.
160
176
  Same words, different jobs — this is the most common source of the
161
177
  `revalidate()`-is-caching misread.
162
178
 
163
- | You may know | Maps to Rango axis | Watch out |
164
- | --------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
165
- | Next.js `export const revalidate = N` | **Axis 1** (cache) | Same word, opposite meaning. Next's `revalidate` is time-based cache expiry; Rango's `revalidate()` is **axis 2**. Use `cache({ ttl })` for the Next behavior. |
166
- | Next.js `revalidateTag` / `updateTag` | **Axis 1** (cache) | Cache busting by tag. Tag via `cache({ tags })` / `cacheTag(...tags)`; invalidate with `updateTag(...tags)` (awaitable, read-your-own-writes) or `revalidateTag(...tags)` (background, non-blocking). Built-in stores index by tag. No `revalidatePath` (path-based busting); use tags. |
167
- | React Router / Remix `shouldRevalidate` | **Axis 2** | This is the correct mental model for Rango's `revalidate()`. |
168
- | HTTP `Cache-Control` / ISR | Deployment layer | Complete-response deployment layer. A CDN hit bypasses Rango entirely; the store-backed middleware does not. See `/deployment-caching` and `/document-cache`. |
169
- | Next.js PPR (partial prerendering) | HTML shell layer | Same React primitive, different transport: Rango serves shells in-function after middleware. Ordinary `ppr` captures at runtime; `Prerender + ppr` captures at build. See `/ppr`, `/prerender`, and `/deployment-caching`. |
170
- | Remix/RR `loader` | live data | Like Rango loaders, fresh per request — but Rango loaders run in parallel and stream (latency overlaps first paint), and can opt into caching on demand. |
179
+ | You may know | Maps to Rango | Watch out |
180
+ | --------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
181
+ | Next.js `export const revalidate = N` | **Value freshness** (cache) | Same word, opposite meaning. Next's `revalidate` is time-based cache expiry; Rango's `revalidate()` is **partial-render selection**. Use `cache({ ttl })` for the Next behavior. |
182
+ | Next.js `revalidateTag` / `updateTag` | **Value freshness** (cache) | Cache busting by tag. Tag via `cache({ tags })` / `cacheTag(...tags)`; invalidate with `updateTag(...tags)` (awaitable, read-your-own-writes) or `revalidateTag(...tags)` (background, non-blocking). Built-in stores index by tag. No `revalidatePath` (path-based busting); use tags. |
183
+ | React Router / Remix `shouldRevalidate` | **Partial-render selection** | This is the correct mental model for Rango's `revalidate()`. |
184
+ | HTTP `Cache-Control` / ISR | Deployment layer | Complete-response deployment layer. A CDN hit bypasses Rango entirely; the store-backed middleware does not. See `/deployment-caching` and `/document-cache`. |
185
+ | Next.js PPR (partial prerendering) | HTML shell layer | Same React primitive, different transport: Rango serves shells in-function after middleware. Ordinary `ppr` captures at runtime; `Prerender + ppr` captures at build. See `/ppr`, `/prerender`, and `/deployment-caching`. |
186
+ | Remix/RR `loader` | live data | Like Rango loaders, fresh per request — but Rango loaders run in parallel and stream (latency overlaps first paint), and can opt into caching on demand. |
171
187
 
172
- See `/cache-guide` for the axis-1 decision guide, `/loader` and `/route` for
173
- `revalidate()` (axis 2), and `/document-cache` for the edge layer.
188
+ Next.js PPR and Rango's `ppr` also differ in the hole model: in Rango only
189
+ loaders make holes. A handler's own promise under `<Suspense>` bakes into the
190
+ shell, as under `cache()`.
191
+
192
+ See `/cache-guide` for the cache decision guide, `/loader` and `/route` for
193
+ `revalidate()` (partial-render selection), and `/document-cache` for the edge
194
+ layer.
174
195
 
175
196
  ## Canonical shape
176
197
 
@@ -178,9 +199,10 @@ See `/cache-guide` for the axis-1 decision guide, `/loader` and `/route` for
178
199
  export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
179
200
  layout(<ShopLayout />, () => [ // structure: wraps children
180
201
  loader(CartLoader, () => [ // config: live data
181
- // partial-render axis: re-run on cart actions, defer otherwise.
202
+ // partial-render axis: after an action, re-run only for cart actions;
203
+ // on navigation, undefined keeps the default.
182
204
  // ctx.isAction() matches by reference (rename-safe), not by string.
183
- revalidate((ctx) => ctx.isAction(CartActions) || undefined),
205
+ revalidate((ctx) => (ctx.isAction() ? ctx.isAction(CartActions) : undefined)),
184
206
  ]),
185
207
  path("/shop/:slug", ProductPage, { name: "product" }, () => [ // structure: leaf
186
208
  loader(ProductLoader, () => [cache({ ttl: 60 })]), // config: cache loader DATA
@@ -201,17 +223,29 @@ The predicate arg carries the action's full context, not just its identity. Matc
201
223
  _which_ action with `ctx.isAction(addToCart)` (rename-safe); branch on _what it
202
224
  returned_ with `ctx.actionResult` — the value your `"use server"` function
203
225
  returned, for outcome-conditional revalidation. The arg also exposes `actionId`
204
- (raw `path#export`), `actionUrl`, `formData`, `method`, and `stale` (cross-tab
205
- `_rsc_stale` signal). All are `undefined` on plain navigation (no action).
206
-
207
- Two idioms, picked by what an _unrelated_ action should do. `ctx.isAction()`
208
- returns a raw boolean, so combine it with `|| undefined` to **defer** ("mine,
209
- else let the default decide": `ctx.isAction(CartActions) || undefined`) or leave
210
- it bare to **suppress** ("mine only": `ctx.isAction(CartActions)`). Prefer the
211
- defer form unless a sibling segment must own the unrelated-action decision.
226
+ (raw `path#export`), `actionUrl`, `formData` (form-based actions only), `method`,
227
+ and `stale` (the `_rsc_stale` signal that an action ran in this or another tab).
228
+ `actionId`, `actionUrl`, `actionResult`, and `formData` are `undefined` on plain
229
+ navigation; `method` is `"GET"` there and `"POST"` for an action. Call
230
+ `ctx.isAction()` with no arguments to ask "was this any action?".
231
+
232
+ `ctx.isAction()` returns a raw boolean. Pick the idiom by what the segment's
233
+ default already does: after an action, loaders, the route, and segments inside
234
+ the `path()` re-run; layouts and parallels above the route are skipped.
235
+
236
+ - **Add a signal** where the default skips: `ctx.isAction(CartActions) || undefined`
237
+ on a parent layout re-renders it after cart actions and defers otherwise. On a
238
+ loader or route segment it changes nothing.
239
+ - **Narrow after actions** where the default re-runs:
240
+ `ctx.isAction() ? ctx.isAction(CartActions) : undefined` re-runs only for cart
241
+ actions and keeps the navigation default (params or search changed).
242
+ - **Avoid bare `ctx.isAction(CartActions)`.** It is a hard `false` on navigation
243
+ too, so a loader stops refetching when params change, and it ends the chain for
244
+ any later revalidator on the segment.
212
245
 
213
246
  ```ts
214
- // re-render only when checkout actually succeeded; defer otherwise
247
+ // on a parent layout (skipped after actions by default): also re-render when
248
+ // checkout succeeded; defer to the default otherwise
215
249
  revalidate((ctx) => (ctx.isAction(checkout) && ctx.actionResult?.ok) || undefined),
216
250
  ```
217
251
 
@@ -262,18 +296,18 @@ Grouped by concern — read when you need to…
262
296
 
263
297
  **Data & caching** — fetch, mutate, and cache:
264
298
 
265
- | Skill | Description |
266
- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
267
- | `/loader` | Data loaders with `createLoader()` and `revalidate()` |
268
- | `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation |
269
- | `/caching` | Segment caching with memory or KV stores |
270
- | `/use-cache` | Function-level caching with `"use cache"` directive |
271
- | `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide |
272
- | `/document-cache` | Store-backed complete-response middleware using Cache-Control policy |
273
- | `/deployment-caching` | Choose between in-function caches, store-backed responses, and an external CDN cache |
274
- | `/ppr` | PPR shell caching: cached shell served instantly, live holes resumed — a hole is a `loading()` subtree OR a pending promise under `<Suspense>` (no loader needed) |
275
- | `/prerender` | Pre-render route segments at build time (Passthrough live fallback) |
276
- | `/shell-manifest` | Replayed handles as cache metadata read by live loaders (frozen shell, batched live holes) |
299
+ | Skill | Description |
300
+ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
301
+ | `/loader` | Data loaders with `createLoader()` and `revalidate()` |
302
+ | `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation |
303
+ | `/caching` | Segment caching with memory or KV stores |
304
+ | `/use-cache` | Function-level caching with `"use cache"` directive |
305
+ | `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide |
306
+ | `/document-cache` | Store-backed complete-response middleware using Cache-Control policy |
307
+ | `/deployment-caching` | Choose between in-function caches, store-backed responses, and an external CDN cache |
308
+ | `/ppr` | PPR shell caching: cached shell served instantly, live holes resumed — a hole is a live loader read under `loading()` or an inline `<Suspense>`; handler output bakes |
309
+ | `/prerender` | Pre-render route segments at build time (Passthrough live fallback) |
310
+ | `/shell-manifest` | Replayed handles as cache metadata read by live loaders (frozen shell, batched live holes) |
277
311
 
278
312
  **Client & presentation** — build the client-side UX:
279
313
 
@@ -326,6 +360,8 @@ Grouped by concern — read when you need to…
326
360
  ```typescript
327
361
  // urls.tsx
328
362
  import { urls } from "@rangojs/router";
363
+ import { RootLayout } from "./layouts/root"; // renders <Outlet />
364
+ import { HomePage, AboutPage } from "./pages";
329
365
 
330
366
  export const urlpatterns = urls(({ path, layout }) => [
331
367
  layout(RootLayout, () => [
@@ -336,12 +372,15 @@ export const urlpatterns = urls(({ path, layout }) => [
336
372
 
337
373
  // router.tsx
338
374
  import { createRouter } from "@rangojs/router";
375
+ import { Document } from "./document"; // <html>/<head>/<body> shell
339
376
  import { urlpatterns } from "./urls";
340
377
 
341
- export default createRouter({ document: Document }).routes(urlpatterns);
378
+ export const router = createRouter({ document: Document, urls: urlpatterns });
342
379
  ```
343
380
 
344
- Use `/typesafety` for type-safe href and environment setup.
381
+ `createRouter({ ... }).routes(urlpatterns)` is equivalent to passing `urls`.
382
+ See `/router-setup` for the Document component and router options, and
383
+ `/typesafety` for type-safe href and environment setup.
345
384
 
346
385
  ## CLI: `npx rango generate`
347
386
 
@@ -362,27 +401,37 @@ npx rango generate src/
362
401
  npx rango generate src/urls.tsx src/api/
363
402
  ```
364
403
 
404
+ Three modes:
405
+
406
+ | Flag | Behavior |
407
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------- |
408
+ | (none) | Static parser. Exits with an error, writing nothing, if an `include()` can't be resolved statically (factory or dynamic). |
409
+ | `--static` | Static parser, but writes partial output and prints the unresolvable includes as warnings. |
410
+ | `--runtime` | Vite-based runtime discovery (full coverage; needs `vite` and `@vitejs/plugin-rsc`). `--config <path>` picks the config. |
411
+
365
412
  ### Auto-detection
366
413
 
367
414
  Each file is classified by its contents:
368
415
 
369
- | Contains | Generated output |
370
- | -------------- | ---------------------------------------------------------------- |
371
- | `urls(` | Per-module `*.gen.ts` with route names, patterns, params, search |
372
- | `createRouter` | Per-router `*.named-routes.gen.ts` with global route map |
373
- | Both | Both files |
416
+ | Contains | Generated output |
417
+ | ------------------------ | ---------------------------------------------------------------- |
418
+ | `urls(` or `clientUrls(` | Per-module `*.gen.ts` with route names, patterns, params, search |
419
+ | `createRouter` | Per-router `*.named-routes.gen.ts` with global route map |
420
+ | Both | Both files |
374
421
 
375
- Directories are scanned recursively for `.ts`/`.tsx` files, skipping `node_modules`,
376
- dotfiles, and existing `.gen.` files.
422
+ Directories are scanned recursively for `.ts`/`.tsx`/`.js`/`.jsx` files, skipping
423
+ `node_modules`, `dist`, `build`, `coverage`, dot-directories, and existing
424
+ `.gen.` files.
377
425
 
378
426
  > The two generated files are **not interchangeable surfaces**.
379
427
  > `router.named-routes.gen.ts` augments the global `GeneratedRouteMap` for
380
428
  > named-route typing (`Handler<"name">`, `ctx.reverse("name")`, prerender).
381
429
  > Per-module `*.gen.ts` exports a local `routes` map for `useReverse(routes)`
382
430
  > and explicit local handler typing (`Handler<".name", routes>`). Neither
383
- > carries response payloads — response/MIME payload inference comes from
384
- > `typeof router.routeMap` via `RegisteredRoutes`, not `*.named-routes.gen.ts`.
385
- > See `/typesafety` for the full surface breakdown.
431
+ > carries response payloads: `RouteResponse<typeof patterns, "name">` reads
432
+ > them from the `urls()` value directly, and `Rango.PathResponse` reads them from
433
+ > `RegisteredRoutes` (`typeof router.routeMap`). See `/typesafety` for the full
434
+ > surface breakdown.
386
435
 
387
436
  ### Recursive includes
388
437
 
@@ -413,7 +462,8 @@ extract routes defined dynamically:
413
462
  - Routes computed from external data (databases, config files)
414
463
  - Template literal patterns with interpolated variables
415
464
 
416
- These routes are only discovered by the Vite plugin's runtime discovery during
417
- `pnpm dev` or `pnpm build`. The CLI-generated `.gen.ts` may have fewer routes
418
- than the runtime-generated version. During dev, the `preserveIfLarger` guard
419
- prevents the static parser from overwriting a larger runtime-discovered file.
465
+ These routes are only discovered at runtime: by the Vite plugin during
466
+ `pnpm dev` / `pnpm build`, or by `rango generate --runtime`. A statically
467
+ generated `.gen.ts` may have fewer routes than the runtime-generated version.
468
+ During dev, the `preserveIfLarger` guard prevents the static parser from
469
+ overwriting a larger runtime-discovered file.
@@ -74,7 +74,7 @@ export default defineConfig({
74
74
  react({ compiler: true }),
75
75
  rango({ preset: "cloudflare" }),
76
76
  cloudflare({
77
- /* ... */
77
+ viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] },
78
78
  }),
79
79
  ],
80
80
  });
@@ -200,7 +200,8 @@ import babel from "@rolldown/plugin-babel";
200
200
  ## Reference
201
201
 
202
202
  A worked, tested wiring (dev + production e2e markers, incl. the client-only
203
- contract and the Fast Refresh check) lives in the `@rangojs/router` repository —
204
- not shipped in this package: `docs/react-compiler.md` and the
205
- `react-compiler.test.ts` files under `e2e/e2e-basic`, `tests/cloudflare-basic`,
206
- and `tests/vite-rsc-demo`.
203
+ contract and the Fast Refresh check) lives in the `@rangojs/router` repository,
204
+ not shipped in this package: `packages/rangojs-router/docs/react-compiler.md`,
205
+ `packages/rangojs-router/e2e/react-compiler.test.ts` (covers the
206
+ `e2e/e2e-basic` app), `tests/cloudflare-basic/e2e/react-compiler.test.ts`, and
207
+ `tests/vite-rsc-demo/e2e/react-compiler.test.ts`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: response-routes
3
- description: Response routes (path.json, path.text, etc.) for non-RSC endpoints with typed responses. Use when building a JSON/text API endpoint alongside your pages, or asking how to return raw JSON instead of RSC from a route.
3
+ description: Response routes (path.json, path.text, etc.) for non-RSC endpoints with typed responses. Use when building a JSON/text API endpoint alongside your pages, asking how to return raw JSON instead of RSC from a route, or protecting a state-changing endpoint from CSRF.
4
4
  argument-hint: [json|text|html|xml|md|image|stream]
5
5
  ---
6
6
 
@@ -8,13 +8,15 @@ argument-hint: [json|text|html|xml|md|image|stream]
8
8
 
9
9
  Response routes skip the RSC pipeline entirely. Use them for JSON APIs, plain text endpoints,
10
10
  XML feeds, image proxies, and any route that returns a `Response` instead of React components.
11
+ Serving several representations from one URL is `/mime-routes`; a typed client for
12
+ calling your JSON routes is `/api-client`.
11
13
 
12
14
  ## Route-Level Tags: path.json(), path.text(), etc.
13
15
 
14
- Inside any `urls()` callback, use `path.json()`, `path.text()`, or other tags alongside regular RSC routes:
16
+ Inside any `urls()` callback, use `path.json()`, `path.text()`, or other tags alongside regular RSC routes. Each takes `(pattern, handler, options?, use?)` like `path()`:
15
17
 
16
18
  ```typescript
17
- import { urls, RouterError } from "@rangojs/router";
19
+ import { urls } from "@rangojs/router";
18
20
 
19
21
  export const urlpatterns = urls(({ path, layout, include }) => [
20
22
  // RSC routes (normal)
@@ -81,18 +83,27 @@ export const urlpatterns = urls(({ path, layout, include }) => [
81
83
 
82
84
  ## ResponseHandlerContext
83
85
 
84
- Response route handlers receive a lighter context (no `ctx.use()`, no `ctx.res`):
86
+ Response route handlers receive a lighter context (no `ctx.use()`, no `ctx.set()`, no `ctx.res`):
85
87
 
86
88
  ```typescript
87
89
  interface ResponseHandlerContext<TParams, TEnv> {
88
- request: Request;
89
- params: TParams; // Typed from URL pattern
90
- env: TEnv; // Plain bindings (DB, KV, etc.)
91
- searchParams: URLSearchParams;
92
- url: URL;
90
+ // Shared request scope (same as every Rango context)
91
+ request: Request; // raw request: headers, method, body
92
+ url: URL; // internal _rsc* params stripped
93
+ originalUrl: URL; // transport URL, unmodified
93
94
  pathname: string;
94
- reverse: (name: string, params?: Record<string, string>) => string;
95
- get: GetVariableFn; // Read middleware variables
95
+ searchParams: URLSearchParams;
96
+ env: TEnv; // Plain bindings (DB, KV, etc.)
97
+ waitUntil(fn: () => Promise<void>): void; // work after the response
98
+ executionContext?: ExecutionContext; // Cloudflare only
99
+ // Response-route specific
100
+ params: TParams; // Typed from URL pattern
101
+ reverse: (
102
+ name: string,
103
+ params?: Record<string, string>,
104
+ search?: Record<string, unknown>,
105
+ ) => string; // GlobalReverseFunction: global names only, ".name" is a type error
106
+ get: GetVariableFn; // Read middleware variables (string key or createVar token)
96
107
  header: (name: string, value: string) => void;
97
108
  // Use cookies().set(name, value, opts) for cookie mutations (standalone API)
98
109
  }
@@ -104,6 +115,8 @@ String-returning handlers (json, text, html, xml, md) can set custom headers and
104
115
  without constructing a full Response:
105
116
 
106
117
  ```typescript
118
+ import { cookies } from "@rangojs/router";
119
+
107
120
  path.md(
108
121
  "/docs/:slug.md",
109
122
  (ctx) => {
@@ -127,10 +140,12 @@ merge, and router `onError` is not invoked. Throw ordinary errors (including
127
140
 
128
141
  ### Environment Access
129
142
 
130
- `ctx.env` is always the plain bindings passed as TEnv to `createRouter<TEnv>()`:
143
+ `ctx.env` is the plain bindings object the router received for the request. Its
144
+ type comes from the global `Rango.Env` registration (`/typesafety`), not from
145
+ the `createRouter<TEnv>()` generic, which `urls()` modules cannot see:
131
146
 
132
147
  ```typescript
133
- // createRouter<{ DB: D1Database; KV: KVNamespace }>({ ... })
148
+ // Rango.Env registered as { DB: D1Database; KV: KVNamespace }
134
149
 
135
150
  // In a response handler:
136
151
  path.json(
@@ -192,6 +207,13 @@ path.json(
192
207
  );
193
208
  ```
194
209
 
210
+ `RouterError` defaults to status 500 when `status` is omitted. Any other thrown
211
+ error becomes a 500 with `code: "INTERNAL"`, and its message is hidden in
212
+ production (`RouterError` messages are always sent). Only `path.json()` produces
213
+ `problem+json`: the other response types answer errors with `text/plain` — the
214
+ `RouterError` message, or for other errors the message in development and
215
+ `Internal Server Error` in production.
216
+
195
217
  ### Returning Response Directly
196
218
 
197
219
  JSON handlers can return `Response` to bypass auto-wrap (custom status, headers, streaming):
@@ -223,6 +245,8 @@ For an OAuth, SSO, or payment callback, validate the registered target and
223
245
  return an explicitly external redirect:
224
246
 
225
247
  ```typescript
248
+ import { redirect } from "@rangojs/router";
249
+
226
250
  path.any("/oauth/decision", async (ctx) => {
227
251
  const redirectUri = await readAndValidateRedirectUri(ctx.request);
228
252
  return redirect(redirectUri, { status: 303, external: true });
@@ -341,16 +365,19 @@ For local/scoped response typing without global augmentation, prefer
341
365
  the response payload straight from the `urls()` patterns and needs no
342
366
  `RegisteredRoutes` wiring.
343
367
 
344
- ### ParamsFor with Response Routes
368
+ ### RouteParams with Response Routes
345
369
 
346
370
  ```typescript
347
- import type { ParamsFor } from "@rangojs/router";
371
+ import type { RouteParams } from "@rangojs/router";
348
372
 
349
- // Works for both RSC and response routes
350
- type ProductParams = ParamsFor<"api.productDetail">;
373
+ // Works for both RSC and response routes (reads the global route map)
374
+ type ProductParams = RouteParams<"api.productDetail">;
351
375
  // = { id: string }
352
376
  ```
353
377
 
378
+ `ParamsFor<TRoutes, "name">` is the lower-level form that takes an explicit
379
+ route map first (e.g. `ParamsFor<typeof router.routeMap, "api.productDetail">`).
380
+
354
381
  ## Links to Response Routes
355
382
 
356
383
  ### Client: href.json(), href.text(), etc.
@@ -401,6 +428,80 @@ Response-route `cache()` stores whole `Response`s. The serve leaf
401
428
  | **Per-client signals** | A response with `Set-Cookie` or `x-rango-keep-cache` is returned live but **not** stored (MISS + SWR revalidation also skip put). |
402
429
  | **Default key** | `response:{type}:{cacheKeyBase(host, path, searchParams)}` — sorted search, reserved `_rsc*` / allowlisted `__*` params excluded. Custom `key()` / store `keyGenerator` still override. |
403
430
 
431
+ ## CSRF
432
+
433
+ The router's built-in `originCheck` guards server actions, fetchable loaders,
434
+ and progressive-enhancement form posts (`/server-actions` → "CSRF
435
+ Protection"). Response routes are **outside** that gate: the check is never
436
+ consulted for them, not even a custom `originCheck` function.
437
+
438
+ That matters for a response route that changes state **and** authenticates
439
+ with cookies. Another site can make the victim's browser send a POST to it —
440
+ an HTML form (`urlencoded`, `multipart`, or `text/plain`) or a `no-cors`
441
+ `fetch()` — and the browser attaches the session cookie. Being a "JSON API" is
442
+ no protection: `path.json()` does not parse the request body, and
443
+ `await ctx.request.json()` parses a `text/plain` body just as happily. Routes
444
+ authenticated only by an `Authorization` header are not exposed, because a
445
+ cross-site request cannot set it.
446
+
447
+ Guard state-changing response routes with middleware that applies the same
448
+ rule as `originCheck`:
449
+
450
+ ```typescript
451
+ import type { Middleware } from "@rangojs/router";
452
+
453
+ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
454
+
455
+ // Mirrors the built-in originCheck: Origin (or Referer) must equal the request
456
+ // protocol + Host; no Origin/Referer means a non-browser client; "null" and a
457
+ // missing Host fail closed.
458
+ export const requireSameOrigin: Middleware = async (ctx, next) => {
459
+ if (SAFE_METHODS.has(ctx.request.method)) return next();
460
+
461
+ const headers = ctx.request.headers;
462
+ let origin = headers.get("origin");
463
+ if (!origin) {
464
+ const referer = headers.get("referer");
465
+ if (referer) {
466
+ try {
467
+ origin = new URL(referer).origin;
468
+ } catch {
469
+ // Malformed Referer: treat as absent
470
+ }
471
+ }
472
+ }
473
+ if (!origin) return next();
474
+
475
+ const host = headers.get("host");
476
+ const expected = host ? `${ctx.url.protocol}//${host}` : null;
477
+ if (!expected || origin.toLowerCase() !== expected.toLowerCase()) {
478
+ throw new Response("Forbidden", { status: 403 });
479
+ }
480
+ return next();
481
+ };
482
+ ```
483
+
484
+ Attach it with a pattern-scoped `router.use()` to cover a whole API prefix, or
485
+ as a `middleware()` use item on a single route:
486
+
487
+ ```typescript
488
+ // router.tsx — every response route under the API prefix
489
+ const router = createRouter()
490
+ .use("/api/*", requireSameOrigin)
491
+ .routes(urlpatterns);
492
+
493
+ // urls.tsx — one route
494
+ path.json("/api/cart", updateCart, { name: "cart" }, () => [
495
+ middleware(requireSameOrigin),
496
+ ]);
497
+ ```
498
+
499
+ Server-to-server callers (signed webhooks, cron, other backends) send no
500
+ `Origin`, so they pass this check; verify their signature or credentials in the
501
+ handler. Behind a proxy that rewrites `Host`, derive `expected` from the header
502
+ your proxy sets, as with a custom `originCheck` (`/router-setup` → "Origin
503
+ check").
504
+
404
505
  ## Mountable Module Pattern
405
506
 
406
507
  A self-contained module with RSC pages + JSON APIs, mountable via `include()`:
@@ -474,8 +575,7 @@ the `_responses` phantom are inferred from the resolved `urls()` value, so
474
575
  ### Type safety after mounting
475
576
 
476
577
  ```typescript
477
- import type { RouteResponse } from "@rangojs/router";
478
- import type { ParamsFor } from "@rangojs/router";
578
+ import type { RouteResponse, RouteParams } from "@rangojs/router";
479
579
 
480
580
  // Scoped (before mount) -- use the module directly, no global wiring needed
481
581
  type Stats = RouteResponse<typeof blogApiPatterns, "stats">;
@@ -488,21 +588,25 @@ type BlogStats = Rango.PathResponse<"/blog/api/stats">;
488
588
  // = { views: number; visitors: number }
489
589
 
490
590
  // Params work through nested includes
491
- type LikesParams = ParamsFor<"blog.api.likes">;
591
+ type LikesParams = RouteParams<"blog.api.likes">;
492
592
  // = { slug: string }
493
593
  ```
494
594
 
495
595
  ### ctx.reverse inside mounted modules
496
596
 
497
- Response route handlers inside a mounted module can reference local names:
597
+ A response handler's `ctx.reverse` resolves **global** names only: it has no
598
+ `include()` scope and does not auto-fill params from the current request. Use
599
+ the fully qualified name and pass every param:
498
600
 
499
601
  ```typescript
500
- // Inside blogApiPatterns handler
501
- path(
602
+ // Inside blogApiPatterns (mounted via include("/api", ..., { name: "api" })
603
+ // inside blogPatterns, mounted via include("/blog", ..., { name: "blog" }))
604
+ path.json(
502
605
  "/:slug/likes",
503
606
  (ctx) => {
504
- // ctx.reverse resolves names relative to the mount point
505
- const commentsUrl = ctx.reverse("comments", { slug: ctx.params.slug });
607
+ const commentsUrl = ctx.reverse("blog.api.comments", {
608
+ slug: ctx.params.slug,
609
+ });
506
610
  // -> "/blog/api/my-post/comments"
507
611
 
508
612
  return { slug: ctx.params.slug, count: 42, commentsUrl };
@@ -511,6 +615,10 @@ path(
511
615
  );
512
616
  ```
513
617
 
618
+ A dot-local name (`ctx.reverse(".comments")`) is a type error here. The
619
+ runtime reverse has no scope to resolve it against, so untyped (JS) code that
620
+ calls it throws `Unknown route` and the route answers 500.
621
+
514
622
  ## Content Negotiation
515
623
 
516
624
  Multiple response types can share the same URL pattern. See `/mime-routes` for the