@rangojs/router 0.17.0 → 0.19.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 (293) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/event-controller.d.ts +3 -3
  4. package/dist/types/browser/history-state.d.ts +18 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  6. package/dist/types/browser/partial-update.d.ts +16 -1
  7. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  8. package/dist/types/browser/react/Link.d.ts +19 -9
  9. package/dist/types/browser/react/context.d.ts +2 -2
  10. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  11. package/dist/types/browser/react/location-state.d.ts +1 -1
  12. package/dist/types/browser/transition-when.d.ts +80 -0
  13. package/dist/types/browser/types.d.ts +104 -9
  14. package/dist/types/build/generate-manifest.d.ts +8 -0
  15. package/dist/types/cache/background-task.d.ts +24 -2
  16. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +48 -2
  18. package/dist/types/cache/cache-policy.d.ts +18 -1
  19. package/dist/types/cache/cache-runtime.d.ts +7 -0
  20. package/dist/types/cache/cache-scope.d.ts +192 -26
  21. package/dist/types/cache/cache-tag.d.ts +116 -11
  22. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  23. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  24. package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
  25. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  26. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  27. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
  28. package/dist/types/cache/document-cache.d.ts +10 -0
  29. package/dist/types/cache/handle-snapshot.d.ts +41 -8
  30. package/dist/types/cache/index.d.ts +1 -0
  31. package/dist/types/cache/invalidation-order.d.ts +23 -0
  32. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  33. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  34. package/dist/types/cache/shell-memo.d.ts +178 -0
  35. package/dist/types/cache/shell-snapshot.d.ts +141 -105
  36. package/dist/types/cache/tag-invalidation.d.ts +54 -3
  37. package/dist/types/cache/types.d.ts +267 -96
  38. package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
  39. package/dist/types/client-urls/navigation.d.ts +28 -2
  40. package/dist/types/client-urls/server-projection.d.ts +5 -2
  41. package/dist/types/client-urls/types.d.ts +5 -5
  42. package/dist/types/client.d.ts +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +1 -1
  45. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  46. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  47. package/dist/types/route-name.d.ts +7 -0
  48. package/dist/types/router/error-handling.d.ts +1 -1
  49. package/dist/types/router/instrument.d.ts +8 -0
  50. package/dist/types/router/intercept-resolution.d.ts +2 -0
  51. package/dist/types/router/match-context.d.ts +5 -0
  52. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  53. package/dist/types/router/middleware-types.d.ts +6 -1
  54. package/dist/types/router/router-options.d.ts +4 -1
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
  56. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  57. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
  59. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  60. package/dist/types/router/transition-when.d.ts +24 -10
  61. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  62. package/dist/types/rsc/full-payload.d.ts +21 -0
  63. package/dist/types/rsc/handler.d.ts +1 -1
  64. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  65. package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
  66. package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
  67. package/dist/types/rsc/shell-capture.d.ts +202 -74
  68. package/dist/types/rsc/shell-serve.d.ts +144 -50
  69. package/dist/types/rsc/types.d.ts +9 -0
  70. package/dist/types/segment-system.d.ts +7 -0
  71. package/dist/types/server/context.d.ts +155 -33
  72. package/dist/types/server/cookie-store.d.ts +33 -0
  73. package/dist/types/server/handle-store.d.ts +52 -1
  74. package/dist/types/server/request-context.d.ts +210 -63
  75. package/dist/types/server/request-headers.d.ts +44 -0
  76. package/dist/types/server.d.ts +1 -0
  77. package/dist/types/ssr/index.d.ts +9 -0
  78. package/dist/types/ssr/ssr-root.d.ts +9 -8
  79. package/dist/types/testing/e2e/index.d.ts +1 -1
  80. package/dist/types/testing/flight.entry.d.ts +7 -0
  81. package/dist/types/testing/index.d.ts +3 -2
  82. package/dist/types/testing/internal/action-id.d.ts +7 -0
  83. package/dist/types/testing/location-state-key.d.ts +24 -0
  84. package/dist/types/testing/render-route.d.ts +17 -1
  85. package/dist/types/testing/run-loader.d.ts +18 -3
  86. package/dist/types/testing/run-transition-when.d.ts +44 -69
  87. package/dist/types/testing/serve-shell-request.d.ts +133 -0
  88. package/dist/types/testing/shell-status.d.ts +44 -19
  89. package/dist/types/transition-when-ref.d.ts +79 -0
  90. package/dist/types/types/cache-types.d.ts +15 -3
  91. package/dist/types/types/handler-context.d.ts +14 -1
  92. package/dist/types/types/index.d.ts +1 -1
  93. package/dist/types/types/segments.d.ts +86 -71
  94. package/dist/types/urls/path-helper-types.d.ts +3 -4
  95. package/dist/types/urls/pattern-types.d.ts +21 -18
  96. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  97. package/dist/types/vite/router-discovery.d.ts +29 -2
  98. package/dist/vite/index.js +1005 -105
  99. package/package.json +1 -1
  100. package/skills/breadcrumbs/SKILL.md +17 -7
  101. package/skills/cache-guide/SKILL.md +175 -76
  102. package/skills/caching/SKILL.md +361 -33
  103. package/skills/catalog.json +1 -1
  104. package/skills/client-urls/SKILL.md +12 -7
  105. package/skills/cloudflare/SKILL.md +15 -0
  106. package/skills/comparison/references/framework-comparison.md +52 -37
  107. package/skills/defer-hydration/SKILL.md +50 -30
  108. package/skills/deployment-caching/SKILL.md +5 -2
  109. package/skills/document-cache/SKILL.md +32 -2
  110. package/skills/handler-use/SKILL.md +2 -0
  111. package/skills/hooks/SKILL.md +3 -1
  112. package/skills/hooks/handle-and-actions.md +3 -1
  113. package/skills/hooks/navigation.md +23 -2
  114. package/skills/hooks/state.md +101 -7
  115. package/skills/intercept/SKILL.md +29 -12
  116. package/skills/loader/SKILL.md +197 -44
  117. package/skills/middleware/SKILL.md +34 -9
  118. package/skills/migrate-nextjs/SKILL.md +68 -37
  119. package/skills/migrate-react-router/SKILL.md +2 -1
  120. package/skills/migrate-react-router/component-migration.md +3 -1
  121. package/skills/migrate-react-router/data-and-actions.md +3 -1
  122. package/skills/migrate-react-router/route-mapping.md +7 -4
  123. package/skills/observability/SKILL.md +65 -0
  124. package/skills/parallel/SKILL.md +13 -8
  125. package/skills/ppr/SKILL.md +715 -371
  126. package/skills/prerender/SKILL.md +30 -17
  127. package/skills/rango/SKILL.md +72 -34
  128. package/skills/route/SKILL.md +14 -7
  129. package/skills/router-setup/SKILL.md +4 -2
  130. package/skills/scripts/SKILL.md +8 -1
  131. package/skills/server-actions/SKILL.md +11 -0
  132. package/skills/shell-manifest/SKILL.md +21 -12
  133. package/skills/testing/SKILL.md +49 -43
  134. package/skills/testing/cache-prerender.md +15 -14
  135. package/skills/testing/client-components.md +45 -3
  136. package/skills/testing/loader.md +19 -1
  137. package/skills/testing/middleware.md +1 -0
  138. package/skills/testing/predicates.md +49 -49
  139. package/skills/testing/render-handler.md +1 -0
  140. package/skills/testing/serve-shell-request.md +202 -0
  141. package/skills/testing/server-actions.md +9 -3
  142. package/skills/theme/SKILL.md +56 -4
  143. package/skills/typesafety/env-and-bindings.md +18 -2
  144. package/skills/use-cache/SKILL.md +91 -23
  145. package/skills/vercel/SKILL.md +14 -2
  146. package/skills/view-transitions/SKILL.md +150 -29
  147. package/src/__augment-tests__/augmented.check.ts +23 -0
  148. package/src/browser/cookie-name.ts +23 -0
  149. package/src/browser/event-controller.ts +4 -4
  150. package/src/browser/history-state.ts +75 -1
  151. package/src/browser/link-interceptor.ts +4 -0
  152. package/src/browser/navigation-bridge.ts +77 -18
  153. package/src/browser/navigation-client.ts +4 -3
  154. package/src/browser/navigation-store.ts +43 -1
  155. package/src/browser/navigation-transaction.ts +31 -11
  156. package/src/browser/partial-update.ts +146 -35
  157. package/src/browser/prefetch/cache.ts +6 -6
  158. package/src/browser/prefetch/fetch.ts +2 -2
  159. package/src/browser/react/Link.tsx +38 -33
  160. package/src/browser/react/NavigationProvider.tsx +2 -2
  161. package/src/browser/react/context.ts +2 -2
  162. package/src/browser/react/location-state-shared.ts +241 -49
  163. package/src/browser/react/location-state.ts +5 -2
  164. package/src/browser/rsc-router.tsx +9 -0
  165. package/src/browser/server-action-bridge.ts +84 -15
  166. package/src/browser/transition-when.ts +205 -0
  167. package/src/browser/types.ts +124 -10
  168. package/src/build/generate-manifest.ts +77 -1
  169. package/src/cache/background-task.ts +55 -2
  170. package/src/cache/cache-exec-scope.ts +48 -1
  171. package/src/cache/cache-key-utils.ts +62 -2
  172. package/src/cache/cache-policy.ts +31 -4
  173. package/src/cache/cache-runtime.ts +209 -129
  174. package/src/cache/cache-scope.ts +573 -69
  175. package/src/cache/cache-tag.ts +475 -19
  176. package/src/cache/cf/cf-base64.ts +36 -4
  177. package/src/cache/cf/cf-cache-constants.ts +12 -0
  178. package/src/cache/cf/cf-cache-store.ts +1094 -376
  179. package/src/cache/cf/cf-cache-types.ts +32 -4
  180. package/src/cache/cf/cf-shell-frame.ts +258 -0
  181. package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
  182. package/src/cache/document-cache.ts +53 -7
  183. package/src/cache/handle-snapshot.ts +53 -9
  184. package/src/cache/index.ts +2 -0
  185. package/src/cache/invalidation-order.ts +84 -0
  186. package/src/cache/isolate-tag-memo.ts +308 -0
  187. package/src/cache/request-tag-mask.ts +147 -0
  188. package/src/cache/shell-memo.ts +341 -0
  189. package/src/cache/shell-snapshot.ts +249 -277
  190. package/src/cache/tag-invalidation.ts +109 -18
  191. package/src/cache/types.ts +274 -102
  192. package/src/cache/vercel/vercel-cache-store.ts +561 -74
  193. package/src/client-urls/client-root.tsx +8 -4
  194. package/src/client-urls/client-urls.ts +8 -6
  195. package/src/client-urls/navigation.ts +55 -4
  196. package/src/client-urls/server-projection.ts +31 -10
  197. package/src/client-urls/types.ts +12 -5
  198. package/src/client.tsx +2 -0
  199. package/src/host/cookie-handler.ts +2 -1
  200. package/src/index.rsc.ts +3 -0
  201. package/src/index.ts +3 -0
  202. package/src/loader-redirect.tsx +7 -3
  203. package/src/prerender/build-shell-capture.ts +173 -110
  204. package/src/route-definition/dsl-helpers.ts +6 -3
  205. package/src/route-definition/helpers-types.ts +11 -11
  206. package/src/route-name.ts +12 -0
  207. package/src/router/content-negotiation.ts +4 -1
  208. package/src/router/error-handling.ts +5 -2
  209. package/src/router/handler-context.ts +42 -5
  210. package/src/router/instrument.ts +54 -2
  211. package/src/router/intercept-resolution.ts +30 -8
  212. package/src/router/loader-resolution.ts +89 -33
  213. package/src/router/logging.ts +5 -3
  214. package/src/router/match-api.ts +89 -42
  215. package/src/router/match-context.ts +5 -0
  216. package/src/router/match-handlers.ts +14 -29
  217. package/src/router/match-middleware/background-revalidation.ts +3 -5
  218. package/src/router/match-middleware/cache-lookup.ts +52 -19
  219. package/src/router/match-middleware/cache-store.ts +36 -10
  220. package/src/router/match-result.ts +1 -0
  221. package/src/router/middleware-types.ts +6 -1
  222. package/src/router/middleware.ts +20 -4
  223. package/src/router/navigation-snapshot.ts +6 -5
  224. package/src/router/prerender-match.ts +10 -0
  225. package/src/router/request-classification.ts +7 -4
  226. package/src/router/router-options.ts +4 -1
  227. package/src/router/segment-resolution/fresh.ts +18 -13
  228. package/src/router/segment-resolution/helpers.ts +4 -2
  229. package/src/router/segment-resolution/loader-cache.ts +366 -70
  230. package/src/router/segment-resolution/loader-mask.ts +3 -4
  231. package/src/router/segment-resolution/loader-snapshot.ts +215 -126
  232. package/src/router/segment-resolution/mask-nested.ts +161 -42
  233. package/src/router/segment-resolution/revalidation.ts +27 -13
  234. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  235. package/src/router/telemetry.ts +6 -3
  236. package/src/router/transition-when.ts +34 -52
  237. package/src/router.ts +2 -1
  238. package/src/rsc/attach-transition-when.ts +47 -0
  239. package/src/rsc/full-payload.ts +47 -3
  240. package/src/rsc/handler.ts +24 -10
  241. package/src/rsc/loader-fetch.ts +2 -1
  242. package/src/rsc/manifest-init.ts +4 -0
  243. package/src/rsc/origin-guard.ts +8 -5
  244. package/src/rsc/progressive-enhancement.ts +10 -32
  245. package/src/rsc/response-cache-serve.ts +69 -78
  246. package/src/rsc/response-route-handler.ts +2 -2
  247. package/src/rsc/rsc-rendering.ts +712 -338
  248. package/src/rsc/server-action.ts +8 -20
  249. package/src/rsc/shell-build-manifest.ts +49 -29
  250. package/src/rsc/shell-capture-constants.ts +182 -12
  251. package/src/rsc/shell-capture.ts +1126 -612
  252. package/src/rsc/shell-serve.ts +331 -85
  253. package/src/rsc/ssr-setup.ts +3 -2
  254. package/src/rsc/types.ts +9 -0
  255. package/src/segment-system.tsx +40 -7
  256. package/src/server/context.ts +328 -72
  257. package/src/server/cookie-store.ts +184 -133
  258. package/src/server/handle-store.ts +245 -27
  259. package/src/server/request-context.ts +364 -100
  260. package/src/server/request-headers.ts +134 -0
  261. package/src/server.ts +4 -0
  262. package/src/ssr/index.tsx +96 -37
  263. package/src/ssr/ssr-root.tsx +9 -8
  264. package/src/testing/dispatch.ts +4 -7
  265. package/src/testing/e2e/index.ts +1 -0
  266. package/src/testing/flight.entry.ts +14 -0
  267. package/src/testing/index.ts +5 -0
  268. package/src/testing/internal/action-id.ts +25 -0
  269. package/src/testing/internal/context.ts +1 -1
  270. package/src/testing/location-state-key.ts +45 -0
  271. package/src/testing/render-handler.ts +1 -1
  272. package/src/testing/render-route.tsx +109 -16
  273. package/src/testing/run-client-revalidate.ts +3 -19
  274. package/src/testing/run-loader.ts +67 -5
  275. package/src/testing/run-transition-when.ts +109 -172
  276. package/src/testing/serve-shell-request.ts +381 -0
  277. package/src/testing/shell-status.ts +73 -47
  278. package/src/theme/ThemeProvider.tsx +7 -6
  279. package/src/transition-when-ref.ts +220 -0
  280. package/src/types/cache-types.ts +15 -3
  281. package/src/types/handler-context.ts +14 -1
  282. package/src/types/index.ts +3 -0
  283. package/src/types/segments.ts +94 -91
  284. package/src/urls/path-helper-types.ts +3 -4
  285. package/src/urls/pattern-types.ts +21 -18
  286. package/src/vite/discovery/discover-routers.ts +3 -0
  287. package/src/vite/discovery/shell-prerender-phase.ts +9 -2
  288. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  289. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  290. package/src/vite/rango.ts +2 -0
  291. package/src/vite/router-discovery.ts +89 -9
  292. package/dist/types/rsc/transition-gate.d.ts +0 -23
  293. package/src/rsc/transition-gate.ts +0 -86
@@ -20,8 +20,9 @@ The handler code and its imports are removed from the production bundle.
20
20
 
21
21
  ## Not this skill if…
22
22
 
23
- - You want a cached HTML shell captured at runtime, with holes and loaders
24
- staying live per request — see `/ppr`.
23
+ - You want a cached HTML shell captured at runtime, with holes staying live
24
+ per request (loaders without `ssr: false` run per request; `ssr: false`
25
+ loaders bake) — see `/ppr`.
25
26
  - You want runtime segment caching with TTL/SWR — that is the `cache()` DSL:
26
27
  see `/caching`. Prerender is the same cache filled at build time.
27
28
  - You are unsure which cache layer you need — start at `/cache-guide`.
@@ -244,15 +245,14 @@ In client and SSR environments, ALL prerender handlers are always stubbed.
244
245
  ## Sub-use Semantics
245
246
 
246
247
  Everything inside the path's use() callback is part of the pre-rendered route
247
- subtree (the "B segment" in the design docs) and gets pre-rendered. Loaders are
248
- the exception — they stay live:
248
+ subtree and gets pre-rendered. Loaders are the exception — they stay live:
249
249
 
250
250
  ```typescript
251
251
  path("/blog/:slug", BlogPost, { name: "blog.post" }, () => [
252
- layout(<PostLayout />, () => [ // inside B -> pre-rendered
252
+ layout(<PostLayout />, () => [ // inside the route -> pre-rendered
253
253
  loader(PostMetaLoader), // live at runtime, bundled normally
254
254
  ]),
255
- parallel({ "@sidebar": BlogSidebar }), // inside B -> pre-rendered
255
+ parallel({ "@sidebar": BlogSidebar }), // inside the route -> pre-rendered
256
256
  ])
257
257
  ```
258
258
 
@@ -327,9 +327,16 @@ implicitly re-run outer prerender-derived handlers/layouts.
327
327
  ## Prerender + PPR Build Shells
328
328
 
329
329
  A `Prerender` page may also declare `ppr` on the path option. The build still
330
- stores the Flight payload first. After that, a build-shell step ("producer B"
331
- in the design docs) tries to bake the HTML shell for each generated URL so the
332
- first document request can be an `x-rango-shell: HIT`.
330
+ stores the Flight payload first. After that, the build-time shell capture
331
+ tries to bake the HTML shell for each generated URL so the first document
332
+ request can be an `x-rango-shell: HIT`. A route partitioned by
333
+ `cache({ key })` or a store `keyGenerator` never serves that build shell (the
334
+ build captured one partition); each partition captures at runtime, a
335
+ once-per-route warning says so, and a `keyGenerator` that returns the default
336
+ key unchanged keeps it. When the route's `cache()` scope (its own, or one
337
+ inherited from a layout) refuses a request — `cache(false)`, or a
338
+ `condition()` that returns false — that request gets no shell at all, the
339
+ build shell included: it renders like a cache miss.
333
340
 
334
341
  That shell capture is request-shaped enough to run middleware safely:
335
342
 
@@ -362,14 +369,15 @@ entry STILL serves and a runtime recapture is scheduled that upgrades it in plac
362
369
  (SWR is the upgrade path from build entry → fresher runtime entry). Because the
363
370
  `Prerender` handler is evicted from the production bundle, that recapture never
364
371
  re-runs the handler — it replays the same build-time segments and only refreshes
365
- `cache()`-scoped data baked into the shell. **If nothing in the shell is
366
- `cache()`-backed, `ttl` has nothing to refresh** — reach for `updateTag` (or a
372
+ what the capture reads again: `cache()`-scoped data baked into the shell, and
373
+ the data of `ssr: false` loaders, which run at every capture. **If the shell
374
+ bakes neither, `ttl` has nothing to refresh** — reach for `updateTag` (or a
367
375
  redeploy) instead of a shorter `ttl`.
368
376
 
369
- `ctx.dynamic()` opts a request off the shell axis ONLY. A `Prerender` route has
377
+ `ctx.dynamic()` opts a request off the shell ONLY. A `Prerender` route has
370
378
  no live handler to fall back to (it was evicted), so a `dynamic()` request still
371
- serves the build-baked segments — fresh loaders in their holes, not a fresh
372
- handler render. There is no "fully dynamic" render for a prerendered route.
379
+ serves the build-baked segments — fresh loaders, not a fresh handler render.
380
+ There is no "fully dynamic" render for a prerendered route.
373
381
 
374
382
  ## Dev Mode
375
383
 
@@ -635,6 +643,11 @@ Passthrough entries are logged distinctly:
635
643
  Loaders on pre-rendered routes run at request time. They are bundled normally
636
644
  and need `cache()` for caching. Do not use build-only APIs in loaders.
637
645
 
646
+ The exception is an `ssr: false` loader on a `Prerender` + `ppr` route: the
647
+ build-time shell capture runs it and bakes its settled data into the shell,
648
+ and a shell HIT runs it only when its return carries promises (`/ppr` → "The
649
+ bake lane"). The Flight payload collection itself still skips loaders.
650
+
638
651
  ### Build-time handle data is frozen
639
652
 
640
653
  Handle values pushed via `ctx.use()` DURING pre-rendering (handler pushes at
@@ -782,8 +795,8 @@ At runtime, the cache-lookup middleware checks `ctx.isIntercept`:
782
795
  intercept-resolution middleware runs live. This handles `when` config conditions
783
796
  that prevented pre-rendering.
784
797
 
785
- The `when` config selector receives an `InterceptSelectorContext` with `from.pathname`
786
- which is unknown at build time. All intercepts are pre-rendered unconditionally;
798
+ The `when` config selector receives an `InterceptSelectorContext` whose `from`
799
+ location (the page being left) is unknown at build time. All intercepts are pre-rendered unconditionally;
787
800
  `when` is evaluated at runtime by the intercept-resolution middleware.
788
801
 
789
802
  ### Example: Pre-rendered route with intercept
@@ -807,7 +820,7 @@ layout(ShopLayout, () => [
807
820
  "@modal",
808
821
  ".detail",
809
822
  <ProductModal />,
810
- { when: ({ from }) => from.pathname === "/shop" },
823
+ { when: ({ from }) => from.url.pathname === "/shop" },
811
824
  () => [loader(ProductLoader)],
812
825
  ),
813
826
  ])
@@ -26,7 +26,9 @@ 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
@@ -68,8 +70,9 @@ Reach for the next rung only when the one above doesn't fit — the higher rungs
68
70
  are immune to partial-revalidation staleness by construction.
69
71
 
70
72
  1. **A loader** (`loader()` + `useLoader()`). Loaders resolve fresh on every
71
- pass — full renders, action revalidations, cache hits. Nothing to keep in
72
- 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.
73
76
  2. **Middleware `ctx.set()`**. Route middleware wraps every render pass,
74
77
  including post-action revalidation and PE re-renders, so its variables are
75
78
  never stale. Right for request-shaped context: auth, session, locale.
@@ -114,7 +117,7 @@ stated, greppable contract.
114
117
  | guarantee loader output in the SSR HTML | `loader(L, { ssr: false })` | /loader |
115
118
  | pre-render a route at build time | `Prerender(...)` wrapper | /prerender |
116
119
  | feed live loaders from a cached shell | replayed handle + `ctx.rendered()` | /shell-manifest |
117
- | cache the HTML shell, keep loaders live | `ppr` path option | /ppr |
120
+ | cache HTML shell, live loaders as holes | `ppr` path option | /ppr |
118
121
  | choose in-function vs CDN caching | deployment cache boundary | /deployment-caching |
119
122
  | stream SSE / upgrade a WebSocket | `path.stream()` / `path.any()` | /streams-and-websockets |
120
123
 
@@ -123,16 +126,29 @@ stated, greppable contract.
123
126
  - `path()`/`include()` are always visible in `urls()`; config helpers are extractable.
124
127
  - **Cache decides freshness; `revalidate()` decides client-update.** Orthogonal; compose.
125
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.
126
131
  - **The consumption-lane rule.** For every shared artifact (`cache()`,
127
132
  `"use cache"`, the PPR shell): server-side handler consumption
128
- (`await ctx.use(loader)`) yields a BAKED copy — identity reads
129
- (`cookies()`/`headers()`) are permitted there and the capture-time value
130
- freezes into the shared artifact (a documented footgun; see `/caching` →
131
- "Cache purity & tainted objects"). Client-side consumption (`useLoader` in
132
- a `"use client"` component) is the LIVE lane. DSL `loader()` segments
133
- follow their PPR lane (only `ssr: false` bakes; see `/ppr` → The loader
134
- lane rule). Pinned by semantic-matrix row PPR3.
135
- - Inside `"use cache"`: `cookies()`/`headers()` and `ctx` side-effects
133
+ (`await ctx.use(loader)`) yields a BAKED copy — the capture-time value
134
+ freezes into the shared artifact. Identity reads (`cookies()`/`headers()`/
135
+ `ctx.request.headers`, a `{ cache: false }` variable) inside that loader
136
+ are permitted under a route `cache()` (a documented footgun; see
137
+ `/caching` → "Cache purity & tainted objects"). They throw inside
138
+ `"use cache"` (when the cached function is the first to read the loader in
139
+ the request; an already-started loader's value is stored unchecked) and REFUSE a PPR capture: the route serves uncached until
140
+ the read moves to a live loader. Client-side consumption
141
+ (`useLoader` in a `"use client"` component) is the LIVE lane. DSL
142
+ `loader()` segments follow their PPR lane (only `ssr: false` bakes; see
143
+ `/ppr` → The loader lane rule).
144
+ - **A PPR shell HIT never runs a handler.** Everything a handler produces
145
+ (promises it passes under `<Suspense>`, async server components, handle
146
+ pushes, loaders it awaits) is shell material, as under `cache()`; a HIT
147
+ runs middleware and live loaders only (an `ssr: false` loader runs there
148
+ only when its return carries promises). Live data belongs in a loader without
149
+ `ssr: false`, read with `useLoader` under `loading()` or an inline
150
+ `<Suspense>`.
151
+ - Inside `"use cache"`: `cookies()`/`headers()`/`ctx.request.headers` and `ctx` side-effects
136
152
  (`set`/`header`/`setTheme`/`onResponse`/`setLocationState`) throw; `ctx.use(Handle)`
137
153
  is captured on miss and replayed on hit. A non-cacheable variable read
138
154
  (`createVar({ cache: false })`) throws too — see the correctness bullet below.
@@ -149,6 +165,23 @@ stated, greppable contract.
149
165
  - Nested caches: the outer cache window bounds the inner — an inner shorter TTL
150
166
  only applies when the enclosing cache recomputes; put a value in a loader if it
151
167
  must be fresher. See `/cache-guide` → "Combining Both".
168
+ - **A predicate runs where its effect lands.** A server-tree `revalidate()`
169
+ decides what the server re-sends, so it runs on the server; an `intercept({
170
+ when })` selector decides what the server renders, so it runs on the server;
171
+ a `clientUrls()` `revalidate()` and `transition({ when })` decide a browser
172
+ commit, so they run in the browser. A browser predicate sees URLs, params,
173
+ route names, history state and action data, never `get()`, `env` or handler
174
+ state; in a server `urls()` it is written inline (the build hoists it into a
175
+ client module) or imported from a `"use client"` module (`/view-transitions`).
176
+ - **Vocabulary.** `revalidate()` keeps React Router's `shouldRevalidate`
177
+ dialect (`currentUrl`/`nextUrl`, `currentParams`/`nextParams`). Navigation
178
+ selectors (`intercept({ when })`, `transition({ when })`) use `from`/`to`
179
+ locations (`{ url, params, routeName }`, plus `state` in the browser).
180
+ - **A throwing predicate yields the conservative default.** For
181
+ `transition({ when })` that is no hold (an urgent commit) plus
182
+ `console.error`; for a `revalidate()` it is the default decision it would
183
+ have deferred to, logged; for an `intercept({ when })` selector it is no
184
+ intercept (the full page renders), logged with the route name.
152
185
 
153
186
  ## Don't confuse
154
187
 
@@ -162,17 +195,22 @@ stated, greppable contract.
162
195
  Same words, different jobs — this is the most common source of the
163
196
  `revalidate()`-is-caching misread.
164
197
 
165
- | You may know | Maps to Rango axis | Watch out |
166
- | --------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
167
- | 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. |
168
- | 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. |
169
- | React Router / Remix `shouldRevalidate` | **Axis 2** | This is the correct mental model for Rango's `revalidate()`. |
170
- | 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`. |
171
- | 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`. |
172
- | 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. |
198
+ | You may know | Maps to Rango | Watch out |
199
+ | --------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
200
+ | 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. |
201
+ | 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. |
202
+ | React Router / Remix `shouldRevalidate` | **Partial-render selection** | This is the correct mental model for Rango's `revalidate()`. |
203
+ | 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`. |
204
+ | 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`. |
205
+ | 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. |
173
206
 
174
- See `/cache-guide` for the axis-1 decision guide, `/loader` and `/route` for
175
- `revalidate()` (axis 2), and `/document-cache` for the edge layer.
207
+ Next.js PPR and Rango's `ppr` also differ in the hole model: in Rango only
208
+ loaders make holes. A handler's own promise under `<Suspense>` bakes into the
209
+ shell, as under `cache()`.
210
+
211
+ See `/cache-guide` for the cache decision guide, `/loader` and `/route` for
212
+ `revalidate()` (partial-render selection), and `/document-cache` for the edge
213
+ layer.
176
214
 
177
215
  ## Canonical shape
178
216
 
@@ -277,18 +315,18 @@ Grouped by concern — read when you need to…
277
315
 
278
316
  **Data & caching** — fetch, mutate, and cache:
279
317
 
280
- | Skill | Description |
281
- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
282
- | `/loader` | Data loaders with `createLoader()` and `revalidate()` |
283
- | `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation |
284
- | `/caching` | Segment caching with memory or KV stores |
285
- | `/use-cache` | Function-level caching with `"use cache"` directive |
286
- | `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide |
287
- | `/document-cache` | Store-backed complete-response middleware using Cache-Control policy |
288
- | `/deployment-caching` | Choose between in-function caches, store-backed responses, and an external CDN cache |
289
- | `/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) |
290
- | `/prerender` | Pre-render route segments at build time (Passthrough live fallback) |
291
- | `/shell-manifest` | Replayed handles as cache metadata read by live loaders (frozen shell, batched live holes) |
318
+ | Skill | Description |
319
+ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
320
+ | `/loader` | Data loaders with `createLoader()` and `revalidate()` |
321
+ | `/server-actions` | Mutations with `"use server"`, useActionState, validation, revalidation |
322
+ | `/caching` | Segment caching with memory or KV stores |
323
+ | `/use-cache` | Function-level caching with `"use cache"` directive |
324
+ | `/cache-guide` | When to use `cache()` vs `"use cache"` — differences and decision guide |
325
+ | `/document-cache` | Store-backed complete-response middleware using Cache-Control policy |
326
+ | `/deployment-caching` | Choose between in-function caches, store-backed responses, and an external CDN cache |
327
+ | `/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 |
328
+ | `/prerender` | Pre-render route segments at build time (Passthrough live fallback) |
329
+ | `/shell-manifest` | Replayed handles as cache metadata read by live loaders (frozen shell, batched live holes) |
292
330
 
293
331
  **Client & presentation** — build the client-side UX:
294
332
 
@@ -329,12 +329,17 @@ cache({ ttl: 60 }, () => [
329
329
  ]);
330
330
  ```
331
331
 
332
- The same read throws inside a `"use cache"` function body, where the value
333
- would be stored without being part of the key — pass it in as an argument
334
- instead. The guard does not follow derived values (a string copied out of
335
- `Session` and cached elsewhere is not tracked). Loaders are exempt — they run
336
- fresh on every request — but a `"use cache"` function a loader calls is not. Cacheable vars (the default) can be read freely
337
- inside cache scopes. See `/cache-guide` → "Context Variable Cache Safety".
332
+ The same read throws inside a `"use cache"` function body, where the value would
333
+ be stored without being part of the key — pass it in as an argument instead. The
334
+ guard does not follow derived values (a string copied out of `Session` and
335
+ cached elsewhere is not tracked). Under `cache()`, loaders are exempt — a DSL
336
+ loader re-runs on every hit — but a `"use cache"` function a loader calls is
337
+ not, and neither is a loader bound with its own `cache()` without a `key()` (any
338
+ `key()` switches the check off, so it must include the value; `/loader` → "Cache
339
+ Key"). On a `ppr` route the exemption does not cover a loader the shell capture
340
+ waits for (`ssr: false`, or awaited by a handler via `ctx.use()`): the read
341
+ refuses the capture. Cacheable vars (the default) can be read freely inside
342
+ cache scopes. See `/cache-guide` → "Context Variable Cache Safety".
338
343
 
339
344
  ### Revalidation Contracts for Handler Data
340
345
 
@@ -543,7 +548,7 @@ interface HandlerContext<TParams = {}, TEnv = DefaultEnv, TSearch = {}> {
543
548
  build: boolean; // true while pre-rendering at build time
544
549
  dev: boolean; // true under Vite dev
545
550
  dynamic(): void; // opt this request out of PPR shell capture (see /ppr)
546
- theme?: Theme; // plus setTheme?() — only when the router enables themes (/theme)
551
+ readonly theme?: Theme; // plus setTheme?() — only when the router enables themes; a read is guarded like cookies() (/theme)
547
552
  }
548
553
  ```
549
554
 
@@ -603,6 +608,8 @@ whether the routes mount.
603
608
 
604
609
  A route can configure its own `transition()` — the wrap goes around the route's component itself (routes are leaves; they have no separate default outlet channel). If the route component renders a `<ParallelOutlet />` directly, that slot remains inside the route's VT subtree, so prefer mounting parallel slots in a layout when combining intercept modals with route-level transitions. See [skills/view-transitions](../view-transitions/SKILL.md) for examples and the wrap-location rules across layouts, routes, and slots.
605
610
 
611
+ `transition({ when })` and `revalidate()` decide different things: `when` (a browser predicate) gates the hold, `revalidate()` decides whether the route segment is re-sent. A `revalidate()` returning false keeps the client's segment on a navigation whatever `when` returns, including a `ppr` navigation replayed from the shell.
612
+
606
613
  ## Handler-attached `.use`
607
614
 
608
615
  Page handlers can carry their own loader, middleware, error boundaries, parallels, and other defaults via a `.use` callback — so the page is self-contained and reusable across mount sites without re-wiring the same items.
@@ -163,7 +163,7 @@ interface RangoOptions<TEnv> {
163
163
  viewTransition?: "auto" | false; // Router-placed <ViewTransition> default (/view-transitions)
164
164
  warmup?: boolean; // Connection warmup after idle (default true)
165
165
  strictMode?: boolean; // Hydrate inside <React.StrictMode> (default true)
166
- stateCookiePrefix?: string; // Prefix of the state cookie `{prefix}_{routerId}` (default "rango-state")
166
+ stateCookiePrefix?: string; // Prefix of the state cookie `{prefix}_{routerId}` and the fresh-reads cookie `{prefix}-fresh` (default "rango-state")
167
167
 
168
168
  // --- Request handling ---
169
169
  nonce?: (
@@ -752,7 +752,9 @@ the check.
752
752
  render counts in development. Production behavior is unchanged.
753
753
  - `stateCookiePrefix`: rename the state cookie that keys the client caches. The
754
754
  `_{routerId}` suffix is always kept so sibling apps on one origin do not
755
- collide.
755
+ collide. The prefix also names the fresh-reads cookie `{prefix}-fresh`
756
+ (`rango-state-fresh` by default) that `updateTag()`/`revalidateTag()` set
757
+ on a store with per-isolate memos; a consent-manager list needs both names.
756
758
  - `nonce: () => true`: generate a CSP nonce per request and apply it to the
757
759
  router's inline scripts. Return a string to supply your own, or
758
760
  `false`/`""` to skip it for one request. Middleware reads it with
@@ -114,7 +114,10 @@ during navigation.
114
114
  > means it is silently dropped. If a loader must contribute an inline script
115
115
  > to the document, register it `loader(Def, { ssr: false })` so the
116
116
  > document render awaits the push; otherwise push from a handler (or use an
117
- > `async` config, which stays reactive).
117
+ > `async` config, which stays reactive). On a `ppr` route a loader without
118
+ > `ssr: false` never runs during the shell capture, so its inline/ordered
119
+ > script is dropped on every shell HIT; with `ssr: false` the push bakes into
120
+ > the shell.
118
121
 
119
122
  **Nonce caveat for soft-nav async.** The "nonce is applied automatically" claim
120
123
  holds for DOCUMENT-RENDERED scripts (they carry the nonce in the SSR HTML). An
@@ -185,6 +188,10 @@ Otherwise allow the vendor hosts. For GTM/GA4 (Google's wildcards): `script-src
185
188
  `frame-src https://*.googletagmanager.com` for the GTM `<noscript>` iframe. See
186
189
  [Google's CSP guide](https://developers.google.com/tag-platform/security/guides/csp).
187
190
 
191
+ A per-request nonce also takes a `ppr` route off its shell: a shared shell
192
+ cannot carry one request's nonce, so the route renders normally (no shell) and
193
+ warns once per key. See `/ppr` → "What always renders without a shell".
194
+
188
195
  ## Not covered (do it yourself)
189
196
 
190
197
  - **`onLoad` / `onReady` / `onError`** — callbacks can't cross the server handle
@@ -37,6 +37,17 @@ Actions mutate state; route handlers and loaders read the latest state. After
37
37
  an action finishes, Rango performs a server-side revalidation render for the
38
38
  matched route so the UI receives fresh segment output and loader data.
39
39
 
40
+ Segments under a route `cache()` re-render fresh on that render (actions skip
41
+ the route cache lookup and write). A loader with its own `cache()` and a
42
+ `"use cache"` read return their stored entry, and a `ppr` route's stored shell
43
+ is not evicted, unless the action invalidates their tags.
44
+ `await updateTag(...tags)` purges them before the revalidation render, so the
45
+ action's own response reads fresh (`revalidateTag()` gives that render fresh
46
+ reads too; only its durable write runs in the background). On a store that keeps
47
+ per-isolate memos (`CFCacheStore`, `VercelCacheStore`) the response also sets
48
+ the fresh-reads cookie (`rango-state-fresh` by default), so the same user's next
49
+ requests skip those memos (see `/caching` → "The fresh-reads cookie").
50
+
40
51
  The main control point is `revalidate((ctx) => ...)` on the segment that owns
41
52
  the data. Match specific actions by imported reference with `ctx.isAction()`;
42
53
  use raw `actionId` only when you intentionally need path or directory matching.
@@ -9,8 +9,8 @@ argument-hint:
9
9
  Use this when a cached or prerendered shell has dynamic holes, and the live
10
10
  data layer needs to know **what the shell actually contains** — which
11
11
  products, which slots, which keys. The frozen render describes itself
12
- through a handle; loaders (always live) read that description and fetch
13
- exactly the dynamic data the shell needs, in one batch.
12
+ through a handle; live loaders (no `ssr: false`) read that description and
13
+ fetch exactly the dynamic data the shell needs, in one batch.
14
14
 
15
15
  Canonical case: a prerendered product list where prices must stay live.
16
16
 
@@ -32,16 +32,19 @@ batched.
32
32
  ## The mechanism (three features composed)
33
33
 
34
34
  1. **Handles record data at render time.** The handler pushes to a handle
35
- (`ctx.use(Handle)`) while it renders — at build time for `Prerender`, on
36
- the cache miss for `cache()`. (Loader bodies can push handles too — see
37
- `/loader` — but a live loader's push is request-time and is NOT part of
38
- the replayed artifact; a manifest handle must be pushed by the code that
39
- gets frozen with the shell. An `ssr: false` loader on a `ppr` route is the
40
- exception: its settled pushes are recorded and replayed, and its re-run
41
- replaces them with its live pushes — `/ppr` "On a shell HIT".)
35
+ (`ctx.use(Handle)`) while it renders — at build time for `Prerender`, on the
36
+ cache miss for `cache()`. (Loader bodies can push handles too — see `/loader`
37
+ — but a live loader's push is request-time and is NOT part of the replayed
38
+ artifact; a manifest handle must be pushed by the code that gets frozen with
39
+ the shell. An `ssr: false` loader on a `ppr` route is the exception: its
40
+ settled pushes are recorded with the shell. On a HIT, one whose return has no
41
+ promises does not run (unless the capture marked it to run), so the recorded
42
+ pushes are what the page shows; one whose return carries promises runs on
43
+ every HIT, and its pushes replace the recorded ones — `/ppr` "On a shell
44
+ HIT".)
42
45
  2. **Replay on every hit.** Handle data is stored with the Flight payload
43
- and replayed into the handle store on cache/prerender hits — handler code
44
- does not re-run, but its pushes do.
46
+ and replayed into the handle store on cache, prerender and `ppr` shell
47
+ hits — handler code does not re-run, but its pushes do.
45
48
  3. **Loaders read after the render barrier.** A DSL loader can
46
49
  `await ctx.rendered()` (waits for all non-loader segments to settle —
47
50
  fresh render or replay alike), then `ctx.get(Handle)` returns the
@@ -185,7 +188,13 @@ const prices = await runLoader(PriceLoader, {
185
188
 
186
189
  This tests the loader's post-barrier logic. The real
187
190
  push → store → replay → barrier wiring is covered at the e2e tier (dev +
188
- production), like every cache-path behavior.
191
+ production), like every cache-path behavior. On a `ppr` route,
192
+ `serveShellRequest(router, url, { cacheStore })` from
193
+ `@rangojs/router/testing/flight` runs the real capture and HIT in a unit test
194
+ (react-server Vitest): serve the URL twice with the same store; the second
195
+ response is a HIT, and its `flight` carries the live loader's output for the
196
+ replayed ids. Call `resetShellTestState()` in `beforeEach`. Recipe:
197
+ `/testing` → `serve-shell-request.md`.
189
198
 
190
199
  ## Related
191
200
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: testing
3
- description: Test @rangojs/router apps — unit (loaders, middleware, server actions, handles, transition/clientUrls predicates), client components (renderRoute), RSC/Flight (renderServerTree, renderHandler), integration (dispatch), and e2e (dev+prod parity, progressive enhancement). Use when writing a unit test for a loader, middleware, action, or handler, setting up vitest for a Rango app, or testing a route end-to-end in dev and production.
3
+ description: Test @rangojs/router apps — unit (loaders, middleware, server actions, handles, transition/clientUrls predicates), client components (renderRoute), RSC/Flight (renderServerTree, renderHandler, serveShellRequest for PPR shells), integration (dispatch), and e2e (dev+prod parity, progressive enhancement). Use when writing a unit test for a loader, middleware, action, or handler, setting up vitest for a Rango app, or testing a route end-to-end in dev and production.
4
4
  argument-hint: [layer]
5
5
  ---
6
6
 
@@ -89,29 +89,32 @@ The single rule that drives everything:
89
89
 
90
90
  Each primitive links to its sub-file (API + recipe + caveats).
91
91
 
92
- | The behavior is… | Layer | Primitive | Import root |
93
- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------- | -------------------------------- |
94
- | a `reverse` / `href` / params / env TYPE contract (a wrong name or missing param must not compile) | types (tsc) | [`@ts-expect-error` / `expectTypeOf`](./reverse-and-types.md) | `@rangojs/router/client` + `tsc` |
95
- | one loader's data logic | unit (node) | [`runLoader`](./loader.md) | `@rangojs/router/testing` |
96
- | a loader's cookie / header / thrown `redirect()`/`notFound()` / handle-push output | unit (node) | [`runLoaderResult`](./loader.md) | `@rangojs/router/testing` |
97
- | one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md) | `@rangojs/router/testing` |
98
- | a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md) | `@rangojs/router/testing` |
99
- | a `transition({ when })` gate (keep/drop) against nav source / target / action metadata | unit (node) | [`runTransitionWhen`](./predicates.md) (`{ kept, whenContext }`; `{ ppr: true }` for pre-handler timing) | `@rangojs/router/testing` |
100
- | a `clientUrls()` `revalidate()` / `isAction(ref)` predicate | unit (node) | [`runClientRevalidate`](./predicates.md) (production chain evaluator + locked defaults) | `@rangojs/router/testing` |
101
- | a handle's `collect`/accumulator, a seeded handle read, or a loader handle write | unit | [`collectHandle` / seeded `handles` / `handlePushes`](./handles.md) | `@rangojs/router/testing` |
102
- | a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
103
- | a stale indicator from `useLoader().isLoading` while a `transition()` navigation re-runs the loader | unit (DOM) | [`renderRoute` + `navigate(url, { loaders })`](./client-components.md#held-navigation) | `@rangojs/router/testing/dom` |
104
- | a redirect / status / headers / cookies / **response route** (json/text/html/xml/md), no Flight | integration | [`dispatch`](./response-routes.md) | `@rangojs/router/testing` |
105
- | a real async **Server Component** (assert what it rendered: typed boundary props, server-rendered host content, inlined-vs-island) | RSC unit | [`renderServerTree` + `findClientBoundaries`/`findElements`](./server-tree.md) | `@rangojs/router/testing/flight` |
106
- | the exact Flight **wire payload** shape (a drift snapshot) | RSC unit | [`renderToFlightString` + `toMatchFlightSnapshot`](./flight.md) | `@rangojs/router/testing/flight` |
107
- | a real route **handler** `(ctx) => rsc` (params/loaders/vars -> rendered RSC + effects) | RSC unit | [`renderHandler`](./render-handler.md) | `@rangojs/router/testing/flight` |
108
- | navigation, hydration, PE parity, view transitions, real SSR | e2e | [`createRangoE2E` -> `parityDescribe`/`expectParity`](./e2e-parity.md) | `@rangojs/router/testing/e2e` |
109
- | a `"use cache"` hit in a handler (the body runs once across renders; the value round-trips) | RSC unit | [`renderHandler` + seeded `cacheStore`](./render-handler.md#a-use-cache-hit) | `@rangojs/router/testing/flight` |
110
- | a `"use cache"` hit in a loader (the same, in the react-server project) | RSC unit | [`runLoader` + seeded `cacheStore`](./loader.md) | `@rangojs/router/testing` |
111
- | cache hit/miss/stale, prerender (= a cache hit by design) | e2e + signal | [`assertCacheStatus` (header) / `assertCacheDecision` (telemetry sink)](./cache-prerender.md) | `@rangojs/router/testing[/e2e]` |
112
- | PPR shell HIT/MISS (`ppr` path option), shell store key / getShell after flush | e2e + signal | [`assertShellStatus` / `shellCacheKey` + store](./cache-prerender.md#ppr-shell-and-navigation-replay) | `@rangojs/router/testing[/e2e]` |
113
- | generated route map drift vs runtime | unit (node) | [`assertGeneratedRoutesMatch`](./reverse-and-types.md) | `@rangojs/router/testing` |
114
- | a platform binding (`env.DB` / Durable Object / `env.R2`) | unit/integr. | [your own double via `env`](./bindings.md) | (any primitive's `env` option) |
92
+ | The behavior is… | Layer | Primitive | Import root |
93
+ | ---------------------------------------------------------------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------- | -------------------------------- |
94
+ | a `reverse` / `href` / params / env TYPE contract (a wrong name or missing param must not compile) | types (tsc) | [`@ts-expect-error` / `expectTypeOf`](./reverse-and-types.md) | `@rangojs/router/client` + `tsc` |
95
+ | one loader's data logic | unit (node) | [`runLoader`](./loader.md) | `@rangojs/router/testing` |
96
+ | a loader's cookie / header / thrown `redirect()`/`notFound()` / handle-push output | unit (node) | [`runLoaderResult`](./loader.md) | `@rangojs/router/testing` |
97
+ | one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md) | `@rangojs/router/testing` |
98
+ | a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md) | `@rangojs/router/testing` |
99
+ | a `transition({ when })` browser predicate against `{ kind, from, to, action }` | unit (node) | [`runTransitionWhen`](./predicates.md) (`{ applied, gatedOff, context }`) | `@rangojs/router/testing` |
100
+ | a `clientUrls()` `revalidate()` / `isAction(ref)` predicate | unit (node) | [`runClientRevalidate`](./predicates.md) (production chain evaluator + locked defaults) | `@rangojs/router/testing` |
101
+ | a handle's `collect`/accumulator, a seeded handle read, or a loader handle write | unit | [`collectHandle` / seeded `handles` / `handlePushes`](./handles.md) | `@rangojs/router/testing` |
102
+ | a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
103
+ | a stale indicator from `useLoader().isLoading` while a `transition()` navigation re-runs the loader | unit (DOM) | [`renderRoute` + `navigate(url, { loaders })`](./client-components.md#held-navigation) | `@rangojs/router/testing/dom` |
104
+ | a component reading `useLocationState(Def)` after `router.push(url, { state: [Def(value)] })` | unit (DOM) | [`renderRoute` + `withLocationStateKey`](./client-components.md#location-state) | `@rangojs/router/testing[/dom]` |
105
+ | a redirect / status / headers / cookies / **response route** (json/text/html/xml/md), no Flight | integration | [`dispatch`](./response-routes.md) | `@rangojs/router/testing` |
106
+ | a real async **Server Component** (assert what it rendered: typed boundary props, server-rendered host content, inlined-vs-island) | RSC unit | [`renderServerTree` + `findClientBoundaries`/`findElements`](./server-tree.md) | `@rangojs/router/testing/flight` |
107
+ | the exact Flight **wire payload** shape (a drift snapshot) | RSC unit | [`renderToFlightString` + `toMatchFlightSnapshot`](./flight.md) | `@rangojs/router/testing/flight` |
108
+ | a real route **handler** `(ctx) => rsc` (params/loaders/vars -> rendered RSC + effects) | RSC unit | [`renderHandler`](./render-handler.md) | `@rangojs/router/testing/flight` |
109
+ | navigation, hydration, PE parity, view transitions, real SSR | e2e | [`createRangoE2E` -> `parityDescribe`/`expectParity`](./e2e-parity.md) | `@rangojs/router/testing/e2e` |
110
+ | a `"use cache"` hit in a handler (the body runs once across renders; the value round-trips) | RSC unit | [`renderHandler` + seeded `cacheStore`](./render-handler.md#a-use-cache-hit) | `@rangojs/router/testing/flight` |
111
+ | a `"use cache"` hit in a loader (the same, in the react-server project) | RSC unit | [`runLoader` + seeded `cacheStore`](./loader.md) | `@rangojs/router/testing` |
112
+ | a loader's own `cache()` (HIT, `updateTag()` of a tag its body recorded) | RSC unit | [`runLoader` + `cache` + seeded `cacheStore`](./loader.md) | `@rangojs/router/testing` |
113
+ | cache hit/miss/stale, prerender (= a cache hit by design) | e2e + signal | [`assertCacheStatus` (header) / `assertCacheDecision` (telemetry sink)](./cache-prerender.md) | `@rangojs/router/testing[/e2e]` |
114
+ | a `ppr` route's shell: MISS -> capture -> HIT, what the shell froze vs what a HIT renders live, tag eviction, navigation replay | RSC unit | [`serveShellRequest`](./serve-shell-request.md) | `@rangojs/router/testing/flight` |
115
+ | PPR shell HIT/MISS in real HTML (`ppr` path option), shell store key / getShell after flush | e2e + signal | [`assertShellStatus` / `shellCacheKey` + store](./cache-prerender.md#ppr-shell-and-navigation-replay) | `@rangojs/router/testing[/e2e]` |
116
+ | generated route map drift vs runtime | unit (node) | [`assertGeneratedRoutesMatch`](./reverse-and-types.md) | `@rangojs/router/testing` |
117
+ | a platform binding (`env.DB` / Durable Object / `env.R2`) | unit/integr. | [your own double via `env`](./bindings.md) | (any primitive's `env` option) |
115
118
 
116
119
  Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
117
120
  `/handler-use`, `/hooks`, `/response-routes`, `/route`, `/caching`, `/prerender`,
@@ -125,7 +128,8 @@ Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
125
128
  [`predicates.md`](./predicates.md), [`reverse-and-types.md`](./reverse-and-types.md)
126
129
  - Unit (DOM): [`client-components.md`](./client-components.md)
127
130
  - RSC unit: [`flight.md`](./flight.md), [`server-tree.md`](./server-tree.md),
128
- [`render-handler.md`](./render-handler.md)
131
+ [`render-handler.md`](./render-handler.md),
132
+ [`serve-shell-request.md`](./serve-shell-request.md)
129
133
  - Integration: [`response-routes.md`](./response-routes.md)
130
134
  - E2E: [`e2e-parity.md`](./e2e-parity.md), [`cache-prerender.md`](./cache-prerender.md)
131
135
 
@@ -134,24 +138,26 @@ Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
134
138
  Where each runtime export is documented (types such as `RunLoaderOptions` sit
135
139
  beside their function):
136
140
 
137
- | Entry | Exports | Documented in |
138
- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |
139
- | `@rangojs/router/testing` | `runLoader`, `runLoaderResult` | [`loader.md`](./loader.md) |
140
- | | `runMiddleware` | [`middleware.md`](./middleware.md) |
141
- | | `runInRequestContext`, `createTestRequestContext`, `runWithRequestContext`, `toRequest`, `seedVariables` | [`server-actions.md`](./server-actions.md) |
142
- | | `runTransitionWhen`, `runClientRevalidate` | [`predicates.md`](./predicates.md) |
143
- | | `collectHandle` | [`handles.md`](./handles.md) |
144
- | | `dispatch` | [`response-routes.md`](./response-routes.md) |
145
- | | `assertCacheStatus`, `assertCacheDecision`, `parseCacheHeader`, `createCacheSink`, `filterCacheDecisions` | [`cache-prerender.md`](./cache-prerender.md) |
146
- | | `assertShellStatus`, `parseShellStatus`, `shellCacheKey`, `SHELL_STATUS_HEADER`, `assertPprReplayStatus`, `parsePprReplayStatus`, `PPR_REPLAY_STATUS_HEADER` | [`cache-prerender.md`](./cache-prerender.md) |
147
- | | `assertGeneratedRoutesMatch`, `diffGeneratedRoutes` | [`reverse-and-types.md`](./reverse-and-types.md) |
148
- | `@rangojs/router/testing/dom` | `renderRoute` | [`client-components.md`](./client-components.md) |
149
- | `@rangojs/router/testing/flight` | `renderToFlightString`, `normalizeFlight`, `assertFlightRuntimeAvailable` | [`flight.md`](./flight.md) |
150
- | | `renderServerTree`, `findClientBoundaries`, `findElements`, `textContent`, `assertFlightTreeRuntimeAvailable` | [`server-tree.md`](./server-tree.md) |
151
- | | `renderHandler` | [`render-handler.md`](./render-handler.md) |
152
- | `@rangojs/router/testing/flight-matchers` | `flightMatchers` (`toMatchFlight`, `toMatchFlightSnapshot`) | [`flight.md`](./flight.md) |
153
- | `@rangojs/router/testing/vitest` | `rangoTestConfig`, `rangoTestAliases`, `rangoInlineDeps`, `rangoUseClientTransform`, `rangoUseCacheTransform` | [`setup.md`](./setup.md) |
154
- | `@rangojs/router/testing/e2e` | `createRangoE2E` and its standalone helpers; the cache/shell status helpers above are re-exported here | [`e2e-parity.md`](./e2e-parity.md) |
141
+ | Entry | Exports | Documented in |
142
+ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
143
+ | `@rangojs/router/testing` | `runLoader`, `runLoaderResult` | [`loader.md`](./loader.md) |
144
+ | | `runMiddleware` | [`middleware.md`](./middleware.md) |
145
+ | | `runInRequestContext`, `createTestRequestContext`, `runWithRequestContext`, `toRequest`, `seedVariables` | [`server-actions.md`](./server-actions.md) |
146
+ | | `runTransitionWhen`, `runClientRevalidate` | [`predicates.md`](./predicates.md) |
147
+ | | `collectHandle` | [`handles.md`](./handles.md) |
148
+ | | `withLocationStateKey` | [`client-components.md`](./client-components.md) |
149
+ | | `dispatch` | [`response-routes.md`](./response-routes.md) |
150
+ | | `assertCacheStatus`, `assertCacheDecision`, `parseCacheHeader`, `createCacheSink`, `filterCacheDecisions` | [`cache-prerender.md`](./cache-prerender.md) |
151
+ | | `assertShellStatus`, `parseShellStatus`, `shellCacheKey`, `SHELL_STATUS_HEADER`, `assertPprReplayStatus`, `parsePprReplayStatus`, `PPR_REPLAY_STATUS_HEADER` | [`cache-prerender.md`](./cache-prerender.md) |
152
+ | | `assertGeneratedRoutesMatch`, `diffGeneratedRoutes` | [`reverse-and-types.md`](./reverse-and-types.md) |
153
+ | `@rangojs/router/testing/dom` | `renderRoute` | [`client-components.md`](./client-components.md) |
154
+ | `@rangojs/router/testing/flight` | `renderToFlightString`, `normalizeFlight`, `assertFlightRuntimeAvailable` | [`flight.md`](./flight.md) |
155
+ | | `renderServerTree`, `findClientBoundaries`, `findElements`, `textContent`, `assertFlightTreeRuntimeAvailable` | [`server-tree.md`](./server-tree.md) |
156
+ | | `renderHandler` | [`render-handler.md`](./render-handler.md) |
157
+ | | `serveShellRequest`, `resetShellTestState` | [`serve-shell-request.md`](./serve-shell-request.md) |
158
+ | `@rangojs/router/testing/flight-matchers` | `flightMatchers` (`toMatchFlight`, `toMatchFlightSnapshot`) | [`flight.md`](./flight.md) |
159
+ | `@rangojs/router/testing/vitest` | `rangoTestConfig`, `rangoTestAliases`, `rangoInlineDeps`, `rangoUseClientTransform`, `rangoUseCacheTransform` | [`setup.md`](./setup.md) |
160
+ | `@rangojs/router/testing/e2e` | `createRangoE2E` and its standalone helpers; the cache/shell status helpers above are re-exported here | [`e2e-parity.md`](./e2e-parity.md) |
155
161
 
156
162
  ## Pre-push checklist
157
163