@rangojs/router 0.16.0 → 0.17.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 (245) hide show
  1. package/README.md +254 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  6. package/dist/types/browser/rsc-router.d.ts +1 -1
  7. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  8. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  9. package/dist/types/cache/cache-scope.d.ts +34 -5
  10. package/dist/types/cache/handle-capture.d.ts +41 -3
  11. package/dist/types/cache/handle-snapshot.d.ts +53 -13
  12. package/dist/types/cache/segment-codec.d.ts +10 -3
  13. package/dist/types/cache/taint.d.ts +1 -1
  14. package/dist/types/cache/types.d.ts +15 -3
  15. package/dist/types/client.d.ts +4 -4
  16. package/dist/types/client.rsc.d.ts +4 -5
  17. package/dist/types/components/DefaultDocument.d.ts +5 -2
  18. package/dist/types/context-var.d.ts +6 -5
  19. package/dist/types/escape-script.d.ts +1 -1
  20. package/dist/types/handles/MetaTags.d.ts +5 -4
  21. package/dist/types/handles/Scripts.d.ts +8 -7
  22. package/dist/types/handles/meta.d.ts +2 -2
  23. package/dist/types/handles/script.d.ts +17 -16
  24. package/dist/types/href-client.d.ts +1 -1
  25. package/dist/types/html.d.ts +12 -0
  26. package/dist/types/index.d.ts +1 -1
  27. package/dist/types/index.rsc.d.ts +3 -2
  28. package/dist/types/loader-store.d.ts +7 -4
  29. package/dist/types/reverse.d.ts +36 -4
  30. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  31. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +1 -1
  34. package/dist/types/router/debug-manifest.d.ts +3 -2
  35. package/dist/types/router/error-handling.d.ts +20 -0
  36. package/dist/types/router/handler-context.d.ts +6 -0
  37. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  38. package/dist/types/router/middleware-types.d.ts +3 -2
  39. package/dist/types/router/router-context.d.ts +3 -1
  40. package/dist/types/router/router-interfaces.d.ts +24 -2
  41. package/dist/types/router/router-options.d.ts +8 -5
  42. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  43. package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
  44. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  45. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
  46. package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
  47. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  48. package/dist/types/router/segment-wrappers.d.ts +1 -1
  49. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  50. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  51. package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
  52. package/dist/types/rsc/shell-capture.d.ts +5 -4
  53. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  54. package/dist/types/rsc/types.d.ts +10 -0
  55. package/dist/types/server/context.d.ts +33 -3
  56. package/dist/types/server/handle-store.d.ts +26 -2
  57. package/dist/types/server/request-context.d.ts +24 -21
  58. package/dist/types/ssr/index.d.ts +15 -0
  59. package/dist/types/static-handler.d.ts +4 -1
  60. package/dist/types/testing/generated-routes.d.ts +2 -2
  61. package/dist/types/testing/internal/context.d.ts +1 -1
  62. package/dist/types/testing/render-route.d.ts +32 -6
  63. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  64. package/dist/types/testing/vitest.d.ts +35 -1
  65. package/dist/types/theme/ThemeScript.d.ts +3 -3
  66. package/dist/types/types/cache-types.d.ts +14 -10
  67. package/dist/types/types/handler-context.d.ts +14 -0
  68. package/dist/types/types/loader-types.d.ts +8 -6
  69. package/dist/types/urls/response-types.d.ts +3 -12
  70. package/dist/types/use-loader.d.ts +12 -3
  71. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  72. package/dist/types/vite/rango.d.ts +1 -1
  73. package/dist/vite/index.js +1381 -1346
  74. package/package.json +2 -2
  75. package/skills/api-client/SKILL.md +22 -10
  76. package/skills/breadcrumbs/SKILL.md +65 -22
  77. package/skills/bundle-analysis/SKILL.md +22 -22
  78. package/skills/cache-guide/SKILL.md +207 -97
  79. package/skills/caching/SKILL.md +295 -93
  80. package/skills/catalog.json +16 -16
  81. package/skills/client-urls/SKILL.md +53 -31
  82. package/skills/cloudflare/SKILL.md +72 -20
  83. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  84. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  85. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  86. package/skills/comparison/SKILL.md +5 -0
  87. package/skills/comparison/references/framework-comparison.md +39 -33
  88. package/skills/composability/SKILL.md +38 -29
  89. package/skills/css/SKILL.md +10 -13
  90. package/skills/debug-manifest/SKILL.md +74 -31
  91. package/skills/defer-hydration/SKILL.md +2 -1
  92. package/skills/deployment-caching/SKILL.md +6 -1
  93. package/skills/document-cache/SKILL.md +92 -28
  94. package/skills/fonts/SKILL.md +16 -8
  95. package/skills/handler-use/SKILL.md +53 -17
  96. package/skills/hooks/SKILL.md +28 -20
  97. package/skills/hooks/data.md +23 -3
  98. package/skills/hooks/handle-and-actions.md +31 -16
  99. package/skills/hooks/navigation.md +39 -10
  100. package/skills/hooks/outlets.md +13 -6
  101. package/skills/hooks/state.md +37 -15
  102. package/skills/hooks/urls.md +7 -5
  103. package/skills/host-router/SKILL.md +64 -12
  104. package/skills/i18n/SKILL.md +60 -34
  105. package/skills/intercept/SKILL.md +161 -26
  106. package/skills/layout/SKILL.md +106 -50
  107. package/skills/links/SKILL.md +51 -19
  108. package/skills/loader/SKILL.md +242 -106
  109. package/skills/middleware/SKILL.md +119 -23
  110. package/skills/migrate-nextjs/SKILL.md +100 -45
  111. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  112. package/skills/migrate-react-router/SKILL.md +49 -38
  113. package/skills/migrate-react-router/component-migration.md +25 -6
  114. package/skills/migrate-react-router/data-and-actions.md +18 -5
  115. package/skills/migrate-react-router/route-mapping.md +12 -6
  116. package/skills/mime-routes/SKILL.md +12 -7
  117. package/skills/observability/SKILL.md +28 -14
  118. package/skills/parallel/SKILL.md +75 -39
  119. package/skills/ppr/SKILL.md +345 -150
  120. package/skills/prerender/SKILL.md +189 -100
  121. package/skills/rango/SKILL.md +65 -34
  122. package/skills/react-compiler/SKILL.md +6 -5
  123. package/skills/response-routes/SKILL.md +133 -25
  124. package/skills/route/SKILL.md +165 -60
  125. package/skills/router-setup/SKILL.md +418 -240
  126. package/skills/scripts/SKILL.md +31 -20
  127. package/skills/server-actions/SKILL.md +220 -59
  128. package/skills/shell-manifest/SKILL.md +12 -7
  129. package/skills/streams-and-websockets/SKILL.md +32 -14
  130. package/skills/tailwind/SKILL.md +37 -33
  131. package/skills/testing/SKILL.md +68 -32
  132. package/skills/testing/cache-prerender.md +55 -29
  133. package/skills/testing/client-components.md +59 -6
  134. package/skills/testing/e2e-parity.md +12 -2
  135. package/skills/testing/flight.md +19 -11
  136. package/skills/testing/handles.md +8 -5
  137. package/skills/testing/loader.md +8 -5
  138. package/skills/testing/middleware.md +8 -3
  139. package/skills/testing/predicates.md +120 -0
  140. package/skills/testing/render-handler.md +70 -22
  141. package/skills/testing/response-routes.md +11 -4
  142. package/skills/testing/reverse-and-types.md +20 -14
  143. package/skills/testing/server-actions.md +12 -3
  144. package/skills/testing/server-tree.md +9 -0
  145. package/skills/testing/setup.md +9 -5
  146. package/skills/theme/SKILL.md +106 -29
  147. package/skills/typesafety/SKILL.md +20 -9
  148. package/skills/typesafety/env-and-bindings.md +126 -47
  149. package/skills/typesafety/generated-files-and-cli.md +26 -14
  150. package/skills/typesafety/params-and-search.md +44 -19
  151. package/skills/typesafety/route-types.md +50 -14
  152. package/skills/use-cache/SKILL.md +200 -82
  153. package/skills/vercel/SKILL.md +21 -11
  154. package/skills/view-transitions/SKILL.md +41 -20
  155. package/src/__augment-tests__/augmented.check.ts +26 -1
  156. package/src/browser/index.ts +2 -2
  157. package/src/browser/navigation-bridge.ts +3 -4
  158. package/src/browser/navigation-transaction.ts +22 -2
  159. package/src/browser/partial-update.ts +35 -25
  160. package/src/browser/react/ScrollRestoration.tsx +6 -6
  161. package/src/browser/rsc-router.tsx +1 -1
  162. package/src/browser/scroll-restoration.ts +53 -6
  163. package/src/cache/cache-exec-scope.ts +37 -4
  164. package/src/cache/cache-key-utils.ts +1 -6
  165. package/src/cache/cache-runtime.ts +230 -61
  166. package/src/cache/cache-scope.ts +76 -12
  167. package/src/cache/document-cache.ts +15 -0
  168. package/src/cache/handle-capture.ts +90 -10
  169. package/src/cache/handle-snapshot.ts +132 -28
  170. package/src/cache/segment-codec.ts +26 -7
  171. package/src/cache/tag-invalidation.ts +4 -3
  172. package/src/cache/taint.ts +1 -1
  173. package/src/cache/types.ts +16 -3
  174. package/src/client.rsc.tsx +10 -9
  175. package/src/client.tsx +5 -4
  176. package/src/components/DefaultDocument.tsx +7 -2
  177. package/src/context-var.ts +6 -5
  178. package/src/escape-script.ts +1 -1
  179. package/src/handles/MetaTags.tsx +13 -11
  180. package/src/handles/Scripts.tsx +11 -10
  181. package/src/handles/meta.ts +2 -2
  182. package/src/handles/script.ts +17 -16
  183. package/src/href-client.ts +1 -1
  184. package/src/html.ts +12 -0
  185. package/src/index.rsc.ts +5 -0
  186. package/src/index.ts +1 -0
  187. package/src/loader-store.ts +7 -4
  188. package/src/prerender/build-shell-capture.ts +8 -11
  189. package/src/reverse.ts +44 -4
  190. package/src/route-definition/dsl-helpers.ts +117 -30
  191. package/src/route-definition/helpers-types.ts +33 -9
  192. package/src/route-definition/resolve-handler-use.ts +121 -6
  193. package/src/route-types.ts +0 -3
  194. package/src/router/debug-manifest.ts +10 -2
  195. package/src/router/error-handling.ts +51 -15
  196. package/src/router/handler-context.ts +34 -25
  197. package/src/router/intercept-resolution.ts +125 -39
  198. package/src/router/loader-resolution.ts +56 -11
  199. package/src/router/match-api.ts +12 -17
  200. package/src/router/match-middleware/background-revalidation.ts +161 -96
  201. package/src/router/match-middleware/cache-lookup.ts +93 -10
  202. package/src/router/match-middleware/cache-store.ts +17 -90
  203. package/src/router/middleware-types.ts +3 -5
  204. package/src/router/middleware.ts +52 -9
  205. package/src/router/prerender-match.ts +53 -19
  206. package/src/router/route-snapshot.ts +11 -1
  207. package/src/router/router-context.ts +2 -0
  208. package/src/router/router-interfaces.ts +25 -2
  209. package/src/router/router-options.ts +8 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -12
  211. package/src/router/segment-resolution/loader-cache.ts +205 -45
  212. package/src/router/segment-resolution/loader-mask.ts +12 -22
  213. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  214. package/src/router/segment-resolution/mask-nested.ts +7 -7
  215. package/src/router/segment-resolution/revalidation.ts +34 -6
  216. package/src/router/segment-wrappers.ts +4 -0
  217. package/src/rsc/render-pipeline.ts +3 -0
  218. package/src/rsc/response-route-handler.ts +11 -3
  219. package/src/rsc/rsc-rendering.ts +8 -45
  220. package/src/rsc/shell-capture-constants.ts +13 -0
  221. package/src/rsc/shell-capture.ts +111 -90
  222. package/src/rsc/ssr-setup.ts +14 -8
  223. package/src/rsc/types.ts +11 -0
  224. package/src/server/context.ts +88 -11
  225. package/src/server/cookie-store.ts +5 -4
  226. package/src/server/handle-store.ts +232 -7
  227. package/src/server/request-context.ts +35 -30
  228. package/src/ssr/index.tsx +40 -7
  229. package/src/static-handler.ts +4 -1
  230. package/src/testing/flight-runtime.d.ts +21 -0
  231. package/src/testing/generated-routes.ts +2 -2
  232. package/src/testing/internal/context.ts +1 -1
  233. package/src/testing/internal/flight-client-globals.ts +7 -5
  234. package/src/testing/render-route.tsx +85 -24
  235. package/src/testing/run-loader.ts +5 -1
  236. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  237. package/src/testing/vitest.ts +62 -1
  238. package/src/theme/ThemeScript.tsx +3 -3
  239. package/src/types/cache-types.ts +14 -10
  240. package/src/types/handler-context.ts +14 -0
  241. package/src/types/loader-types.ts +8 -6
  242. package/src/urls/response-types.ts +3 -23
  243. package/src/use-loader.tsx +12 -3
  244. package/src/vite/plugins/use-cache-transform.ts +118 -74
  245. package/src/vite/rango.ts +7 -3
@@ -33,7 +33,7 @@ with the shape, then pick a primitive.
33
33
  index by tag and invalidate via `updateTag(...tags)` (awaitable, read-your-own-writes)
34
34
  or `revalidateTag(...tags)` (background, non-blocking).
35
35
  - **Type-safe end to end** — route names, params, search schemas, loader return
36
- types, context vars, and `href` / `reverse` are checked at compile time
36
+ types, context vars, and `href()` / `ctx.reverse()` are checked at compile time
37
37
  (`/typesafety`).
38
38
  - **See where time goes** — turn on `debugPerformance` early (router option, or
39
39
  `ctx.debugPerformance()` in middleware for per-request opt-in). It prints a
@@ -42,7 +42,9 @@ with the shape, then pick a primitive.
42
42
  console, OpenTelemetry, or custom sink. See `/observability`.
43
43
 
44
44
  Most features are **just-in-time**: the core is `urls()`, `path()`, `layout()`,
45
- `include()`, and `reverse()`. Caching, parallel routes, intercepts, prerender,
45
+ `include()`, and URL generation (`ctx.reverse()` on the server, `href()` /
46
+ `useReverse()` on the client; there is no standalone `reverse()` export — see
47
+ `/links`). Caching, parallel routes, intercepts, prerender,
46
48
  i18n, themes, and the rest are opt-in — reach for them when a requirement
47
49
  appears, not up front.
48
50
 
@@ -128,12 +130,12 @@ stated, greppable contract.
128
130
  freezes into the shared artifact (a documented footgun; see `/caching` →
129
131
  "Cache purity & tainted objects"). Client-side consumption (`useLoader` in
130
132
  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
+ follow their PPR lane (only `ssr: false` bakes; see `/ppr` → The loader
134
+ lane rule). Pinned by semantic-matrix row PPR3.
133
135
  - Inside `"use cache"`: `cookies()`/`headers()` and `ctx` side-effects
134
136
  (`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.)
137
+ is captured on miss and replayed on hit. A non-cacheable variable read
138
+ (`createVar({ cache: false })`) throws too — see the correctness bullet below.
137
139
  - One identity `path#export` (`functionId`/`$$id`/`actionId`); one store. Freshness
138
140
  is TTL/SWR expiry plus tag-based invalidation: tag via `cache({ tags })` /
139
141
  `cacheTag(...tags)`, then `updateTag(...tags)` (awaitable) or `revalidateTag(...tags)`
@@ -142,7 +144,7 @@ stated, greppable contract.
142
144
  - Caches are correctness-first: persistent store keys are version-segmented (no
143
145
  cross-deploy drift), the forward/back cache is mutation-aware, and
144
146
  `createVar({ cache: false })` throws on a **direct** read inside a `cache()`
145
- boundary (a deliberately non-propagating guard). See `/cache-guide` →
147
+ boundary or a `"use cache"` function (a deliberately non-propagating guard). See `/cache-guide` →
146
148
  "Correctness & invalidation".
147
149
  - Nested caches: the outer cache window bounds the inner — an inner shorter TTL
148
150
  only applies when the enclosing cache recomputes; put a value in a loader if it
@@ -178,9 +180,10 @@ See `/cache-guide` for the axis-1 decision guide, `/loader` and `/route` for
178
180
  export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
179
181
  layout(<ShopLayout />, () => [ // structure: wraps children
180
182
  loader(CartLoader, () => [ // config: live data
181
- // partial-render axis: re-run on cart actions, defer otherwise.
183
+ // partial-render axis: after an action, re-run only for cart actions;
184
+ // on navigation, undefined keeps the default.
182
185
  // ctx.isAction() matches by reference (rename-safe), not by string.
183
- revalidate((ctx) => ctx.isAction(CartActions) || undefined),
186
+ revalidate((ctx) => (ctx.isAction() ? ctx.isAction(CartActions) : undefined)),
184
187
  ]),
185
188
  path("/shop/:slug", ProductPage, { name: "product" }, () => [ // structure: leaf
186
189
  loader(ProductLoader, () => [cache({ ttl: 60 })]), // config: cache loader DATA
@@ -201,17 +204,29 @@ The predicate arg carries the action's full context, not just its identity. Matc
201
204
  _which_ action with `ctx.isAction(addToCart)` (rename-safe); branch on _what it
202
205
  returned_ with `ctx.actionResult` — the value your `"use server"` function
203
206
  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.
207
+ (raw `path#export`), `actionUrl`, `formData` (form-based actions only), `method`,
208
+ and `stale` (the `_rsc_stale` signal that an action ran in this or another tab).
209
+ `actionId`, `actionUrl`, `actionResult`, and `formData` are `undefined` on plain
210
+ navigation; `method` is `"GET"` there and `"POST"` for an action. Call
211
+ `ctx.isAction()` with no arguments to ask "was this any action?".
212
+
213
+ `ctx.isAction()` returns a raw boolean. Pick the idiom by what the segment's
214
+ default already does: after an action, loaders, the route, and segments inside
215
+ the `path()` re-run; layouts and parallels above the route are skipped.
216
+
217
+ - **Add a signal** where the default skips: `ctx.isAction(CartActions) || undefined`
218
+ on a parent layout re-renders it after cart actions and defers otherwise. On a
219
+ loader or route segment it changes nothing.
220
+ - **Narrow after actions** where the default re-runs:
221
+ `ctx.isAction() ? ctx.isAction(CartActions) : undefined` re-runs only for cart
222
+ actions and keeps the navigation default (params or search changed).
223
+ - **Avoid bare `ctx.isAction(CartActions)`.** It is a hard `false` on navigation
224
+ too, so a loader stops refetching when params change, and it ends the chain for
225
+ any later revalidator on the segment.
212
226
 
213
227
  ```ts
214
- // re-render only when checkout actually succeeded; defer otherwise
228
+ // on a parent layout (skipped after actions by default): also re-render when
229
+ // checkout succeeded; defer to the default otherwise
215
230
  revalidate((ctx) => (ctx.isAction(checkout) && ctx.actionResult?.ok) || undefined),
216
231
  ```
217
232
 
@@ -326,6 +341,8 @@ Grouped by concern — read when you need to…
326
341
  ```typescript
327
342
  // urls.tsx
328
343
  import { urls } from "@rangojs/router";
344
+ import { RootLayout } from "./layouts/root"; // renders <Outlet />
345
+ import { HomePage, AboutPage } from "./pages";
329
346
 
330
347
  export const urlpatterns = urls(({ path, layout }) => [
331
348
  layout(RootLayout, () => [
@@ -336,12 +353,15 @@ export const urlpatterns = urls(({ path, layout }) => [
336
353
 
337
354
  // router.tsx
338
355
  import { createRouter } from "@rangojs/router";
356
+ import { Document } from "./document"; // <html>/<head>/<body> shell
339
357
  import { urlpatterns } from "./urls";
340
358
 
341
- export default createRouter({ document: Document }).routes(urlpatterns);
359
+ export const router = createRouter({ document: Document, urls: urlpatterns });
342
360
  ```
343
361
 
344
- Use `/typesafety` for type-safe href and environment setup.
362
+ `createRouter({ ... }).routes(urlpatterns)` is equivalent to passing `urls`.
363
+ See `/router-setup` for the Document component and router options, and
364
+ `/typesafety` for type-safe href and environment setup.
345
365
 
346
366
  ## CLI: `npx rango generate`
347
367
 
@@ -362,27 +382,37 @@ npx rango generate src/
362
382
  npx rango generate src/urls.tsx src/api/
363
383
  ```
364
384
 
385
+ Three modes:
386
+
387
+ | Flag | Behavior |
388
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------- |
389
+ | (none) | Static parser. Exits with an error, writing nothing, if an `include()` can't be resolved statically (factory or dynamic). |
390
+ | `--static` | Static parser, but writes partial output and prints the unresolvable includes as warnings. |
391
+ | `--runtime` | Vite-based runtime discovery (full coverage; needs `vite` and `@vitejs/plugin-rsc`). `--config <path>` picks the config. |
392
+
365
393
  ### Auto-detection
366
394
 
367
395
  Each file is classified by its contents:
368
396
 
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 |
397
+ | Contains | Generated output |
398
+ | ------------------------ | ---------------------------------------------------------------- |
399
+ | `urls(` or `clientUrls(` | Per-module `*.gen.ts` with route names, patterns, params, search |
400
+ | `createRouter` | Per-router `*.named-routes.gen.ts` with global route map |
401
+ | Both | Both files |
374
402
 
375
- Directories are scanned recursively for `.ts`/`.tsx` files, skipping `node_modules`,
376
- dotfiles, and existing `.gen.` files.
403
+ Directories are scanned recursively for `.ts`/`.tsx`/`.js`/`.jsx` files, skipping
404
+ `node_modules`, `dist`, `build`, `coverage`, dot-directories, and existing
405
+ `.gen.` files.
377
406
 
378
407
  > The two generated files are **not interchangeable surfaces**.
379
408
  > `router.named-routes.gen.ts` augments the global `GeneratedRouteMap` for
380
409
  > named-route typing (`Handler<"name">`, `ctx.reverse("name")`, prerender).
381
410
  > Per-module `*.gen.ts` exports a local `routes` map for `useReverse(routes)`
382
411
  > 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.
412
+ > carries response payloads: `RouteResponse<typeof patterns, "name">` reads
413
+ > them from the `urls()` value directly, and `Rango.PathResponse` reads them from
414
+ > `RegisteredRoutes` (`typeof router.routeMap`). See `/typesafety` for the full
415
+ > surface breakdown.
386
416
 
387
417
  ### Recursive includes
388
418
 
@@ -413,7 +443,8 @@ extract routes defined dynamically:
413
443
  - Routes computed from external data (databases, config files)
414
444
  - Template literal patterns with interpolated variables
415
445
 
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.
446
+ These routes are only discovered at runtime: by the Vite plugin during
447
+ `pnpm dev` / `pnpm build`, or by `rango generate --runtime`. A statically
448
+ generated `.gen.ts` may have fewer routes than the runtime-generated version.
449
+ During dev, the `preserveIfLarger` guard prevents the static parser from
450
+ 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