@rangojs/router 0.20.0 → 0.22.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 (331) hide show
  1. package/dist/bin/rango.js +39 -15
  2. package/dist/types/browser/event-controller.d.ts +49 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-store.d.ts +9 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  6. package/dist/types/browser/partial-update.d.ts +2 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  8. package/dist/types/browser/rango-state.d.ts +3 -1
  9. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  10. package/dist/types/browser/react/context.d.ts +25 -0
  11. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  12. package/dist/types/browser/react/location-state.d.ts +7 -2
  13. package/dist/types/browser/rsc-router.d.ts +13 -2
  14. package/dist/types/browser/types.d.ts +10 -0
  15. package/dist/types/build/generate-manifest.d.ts +2 -0
  16. package/dist/types/cache/background-task.d.ts +22 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  18. package/dist/types/cache/cache-policy.d.ts +17 -0
  19. package/dist/types/cache/cache-scope.d.ts +5 -6
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  22. package/dist/types/cache/document-cache.d.ts +8 -5
  23. package/dist/types/cache/handle-snapshot.d.ts +59 -50
  24. package/dist/types/cache/index.d.ts +3 -1
  25. package/dist/types/cache/invalidation-order.d.ts +14 -0
  26. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  27. package/dist/types/cache/shell-snapshot.d.ts +29 -4
  28. package/dist/types/cache/store-scope.d.ts +22 -0
  29. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  30. package/dist/types/cache/types.d.ts +84 -24
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
  32. package/dist/types/client-urls/navigation.d.ts +7 -1
  33. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  34. package/dist/types/errors.d.ts +6 -0
  35. package/dist/types/handles/deferred-resolution.d.ts +18 -1
  36. package/dist/types/internal-suspense-audit.d.ts +1 -0
  37. package/dist/types/loader-store.d.ts +4 -8
  38. package/dist/types/outlet-provider.d.ts +2 -1
  39. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  40. package/dist/types/prerender/cloudflare.d.ts +49 -0
  41. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  42. package/dist/types/prerender/index.d.ts +12 -0
  43. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  44. package/dist/types/prerender/on-demand.d.ts +272 -0
  45. package/dist/types/prerender/producer-guard.d.ts +33 -0
  46. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  47. package/dist/types/prerender/warm-request.d.ts +65 -0
  48. package/dist/types/prerender/warm.d.ts +65 -0
  49. package/dist/types/prerender/writable-store.d.ts +170 -0
  50. package/dist/types/prerender.d.ts +27 -0
  51. package/dist/types/reverse.d.ts +1 -1
  52. package/dist/types/route-content-wrapper.d.ts +3 -1
  53. package/dist/types/router/error-handling.d.ts +2 -0
  54. package/dist/types/router/handler-context.d.ts +1 -1
  55. package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
  56. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  57. package/dist/types/router/pattern-matching.d.ts +2 -0
  58. package/dist/types/router/prefetch-default.d.ts +4 -4
  59. package/dist/types/router/prerender-match.d.ts +23 -1
  60. package/dist/types/router/route-trie-builder.d.ts +8 -1
  61. package/dist/types/router/router-context.d.ts +1 -0
  62. package/dist/types/router/router-interfaces.d.ts +32 -1
  63. package/dist/types/router/router-options.d.ts +39 -5
  64. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  65. package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  67. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  68. package/dist/types/router/trie-matching.d.ts +2 -0
  69. package/dist/types/router-versions.d.ts +39 -0
  70. package/dist/types/rsc/handler-context.d.ts +5 -0
  71. package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
  72. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  73. package/dist/types/rsc/shell-capture.d.ts +14 -1
  74. package/dist/types/rsc/shell-serve.d.ts +2 -2
  75. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  76. package/dist/types/rsc/types.d.ts +12 -6
  77. package/dist/types/segment-boundary-content.d.ts +17 -0
  78. package/dist/types/segment-loader-promise.d.ts +2 -6
  79. package/dist/types/server/build-version-table.d.ts +53 -0
  80. package/dist/types/server/context.d.ts +12 -4
  81. package/dist/types/server/handle-store.d.ts +89 -46
  82. package/dist/types/server/request-context.d.ts +84 -9
  83. package/dist/types/suspense-audit-react.d.ts +26 -0
  84. package/dist/types/suspense-audit.d.ts +157 -0
  85. package/dist/types/testing/action-form.d.ts +34 -0
  86. package/dist/types/testing/build-versions.d.ts +32 -0
  87. package/dist/types/testing/dom.entry.d.ts +1 -1
  88. package/dist/types/testing/flight.entry.d.ts +3 -1
  89. package/dist/types/testing/index.d.ts +6 -0
  90. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  91. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  92. package/dist/types/testing/location-state-key.d.ts +5 -1
  93. package/dist/types/testing/render-route.d.ts +77 -6
  94. package/dist/types/testing/serve-shell-request.d.ts +55 -4
  95. package/dist/types/testing/shell-status.d.ts +14 -6
  96. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  97. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  98. package/dist/types/thenable-status.d.ts +6 -0
  99. package/dist/types/types/handler-context.d.ts +15 -5
  100. package/dist/types/types/route-entry.d.ts +5 -0
  101. package/dist/types/urls/path-helper.d.ts +18 -0
  102. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  103. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  104. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  105. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  106. package/dist/types/vite/discovery/state.d.ts +25 -0
  107. package/dist/types/vite/encryption-key.d.ts +18 -0
  108. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  109. package/dist/types/vite/plugin-types.d.ts +27 -0
  110. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  111. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  112. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  113. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  114. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  115. package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
  116. package/dist/types/vite/router-discovery.d.ts +6 -0
  117. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  118. package/dist/types/vite/utils/client-chunks.d.ts +2 -0
  119. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  120. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  121. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  122. package/dist/vite/index.js +1707 -931
  123. package/package.json +12 -3
  124. package/skills/cache-guide/SKILL.md +102 -37
  125. package/skills/caching/SKILL.md +56 -9
  126. package/skills/catalog.json +1 -1
  127. package/skills/client-urls/SKILL.md +16 -0
  128. package/skills/cloudflare/SKILL.md +133 -1
  129. package/skills/comparison/references/framework-comparison.md +17 -13
  130. package/skills/deployment-caching/SKILL.md +33 -12
  131. package/skills/document-cache/SKILL.md +16 -14
  132. package/skills/hooks/SKILL.md +25 -0
  133. package/skills/hooks/handle-and-actions.md +5 -0
  134. package/skills/hooks/navigation.md +38 -0
  135. package/skills/hooks/state.md +213 -11
  136. package/skills/hooks/urls.md +40 -1
  137. package/skills/host-router/SKILL.md +31 -0
  138. package/skills/intercept/SKILL.md +6 -0
  139. package/skills/loader/SKILL.md +25 -5
  140. package/skills/middleware/SKILL.md +2 -0
  141. package/skills/migrate-nextjs/SKILL.md +82 -9
  142. package/skills/ppr/SKILL.md +150 -48
  143. package/skills/prerender/SKILL.md +651 -10
  144. package/skills/rango/SKILL.md +9 -7
  145. package/skills/response-routes/SKILL.md +5 -5
  146. package/skills/route/SKILL.md +29 -0
  147. package/skills/router-setup/SKILL.md +15 -1
  148. package/skills/testing/SKILL.md +32 -27
  149. package/skills/testing/cache-prerender.md +18 -17
  150. package/skills/testing/client-components.md +193 -8
  151. package/skills/testing/handles.md +2 -0
  152. package/skills/testing/loader.md +1 -1
  153. package/skills/testing/middleware.md +1 -1
  154. package/skills/testing/render-handler.md +18 -18
  155. package/skills/testing/serve-shell-request.md +167 -22
  156. package/skills/testing/server-actions.md +17 -17
  157. package/skills/testing/server-tree.md +1 -1
  158. package/skills/typesafety/generated-files-and-cli.md +1 -1
  159. package/skills/use-cache/SKILL.md +25 -10
  160. package/skills/vercel/SKILL.md +5 -3
  161. package/skills/view-transitions/SKILL.md +9 -4
  162. package/src/bin/rango.ts +1 -1
  163. package/src/browser/event-controller.ts +119 -2
  164. package/src/browser/history-state.ts +39 -17
  165. package/src/browser/link-interceptor.ts +2 -2
  166. package/src/browser/navigation-bridge.ts +104 -41
  167. package/src/browser/navigation-store.ts +26 -0
  168. package/src/browser/navigation-transaction.ts +24 -12
  169. package/src/browser/network-error-handler.ts +2 -0
  170. package/src/browser/partial-update.ts +58 -14
  171. package/src/browser/prefetch/default-strategy.ts +22 -42
  172. package/src/browser/rango-state.ts +3 -1
  173. package/src/browser/react/Link.tsx +37 -43
  174. package/src/browser/react/NavigationProvider.tsx +85 -1
  175. package/src/browser/react/context.ts +30 -0
  176. package/src/browser/react/location-state-shared.ts +200 -34
  177. package/src/browser/react/location-state.ts +28 -77
  178. package/src/browser/react/use-action.ts +4 -1
  179. package/src/browser/react/use-handle.ts +3 -0
  180. package/src/browser/react/use-link-status.ts +4 -3
  181. package/src/browser/react/use-navigation.ts +4 -1
  182. package/src/browser/react/use-params.ts +4 -1
  183. package/src/browser/react/use-pathname.ts +4 -1
  184. package/src/browser/react/use-search-params.ts +5 -3
  185. package/src/browser/react/use-segments.ts +36 -54
  186. package/src/browser/rsc-router.tsx +93 -31
  187. package/src/browser/segment-reconciler.ts +3 -3
  188. package/src/browser/server-action-bridge.ts +43 -7
  189. package/src/browser/types.ts +11 -0
  190. package/src/build/generate-manifest.ts +17 -0
  191. package/src/build/merge-full-manifests.ts +5 -0
  192. package/src/build/route-trie.ts +1 -0
  193. package/src/cache/background-task.ts +61 -0
  194. package/src/cache/cache-key-utils.ts +91 -12
  195. package/src/cache/cache-policy.ts +11 -2
  196. package/src/cache/cache-runtime.ts +23 -23
  197. package/src/cache/cache-scope.ts +48 -36
  198. package/src/cache/cf/cf-cache-store.ts +91 -66
  199. package/src/cache/cf/cf-cache-types.ts +12 -4
  200. package/src/cache/document-cache.ts +35 -14
  201. package/src/cache/handle-snapshot.ts +118 -86
  202. package/src/cache/index.ts +5 -0
  203. package/src/cache/invalidation-order.ts +53 -0
  204. package/src/cache/memory-segment-store.ts +52 -1
  205. package/src/cache/shell-snapshot.ts +52 -5
  206. package/src/cache/store-scope.ts +36 -0
  207. package/src/cache/tag-invalidation.ts +7 -5
  208. package/src/cache/types.ts +90 -24
  209. package/src/cache/vercel/vercel-cache-store.ts +68 -26
  210. package/src/client-urls/client-root.tsx +1 -0
  211. package/src/client-urls/navigation.ts +10 -0
  212. package/src/client-urls/optimistic-location.ts +10 -4
  213. package/src/client.tsx +40 -3
  214. package/src/errors.ts +13 -6
  215. package/src/handles/deferred-resolution.ts +36 -0
  216. package/src/internal-suspense-audit.ts +12 -0
  217. package/src/loader-store.ts +4 -9
  218. package/src/outlet-provider.tsx +25 -1
  219. package/src/prerender/build-shell-capture.ts +11 -9
  220. package/src/prerender/cloudflare.ts +130 -0
  221. package/src/prerender/create-prerender-trigger.ts +780 -0
  222. package/src/prerender/index.ts +39 -0
  223. package/src/prerender/memory-prerender-store.ts +115 -0
  224. package/src/prerender/on-demand.ts +339 -0
  225. package/src/prerender/producer-guard.ts +66 -0
  226. package/src/prerender/shell-manifest-key.ts +12 -17
  227. package/src/prerender/warm-request.ts +154 -0
  228. package/src/prerender/warm.ts +193 -0
  229. package/src/prerender/writable-store.ts +278 -0
  230. package/src/prerender.ts +29 -0
  231. package/src/reverse.ts +1 -1
  232. package/src/route-content-wrapper.tsx +107 -13
  233. package/src/router/error-handling.ts +12 -12
  234. package/src/router/find-match.ts +1 -0
  235. package/src/router/handler-context.ts +2 -0
  236. package/src/router/intercept-resolution.ts +15 -8
  237. package/src/router/loader-resolution.ts +2 -10
  238. package/src/router/manifest.ts +2 -0
  239. package/src/router/match-middleware/background-revalidation.ts +2 -0
  240. package/src/router/match-middleware/cache-lookup.ts +239 -54
  241. package/src/router/navigation-snapshot.ts +25 -6
  242. package/src/router/pattern-matching.ts +14 -0
  243. package/src/router/prefetch-default.ts +4 -4
  244. package/src/router/prerender-match.ts +460 -352
  245. package/src/router/request-classification.ts +15 -12
  246. package/src/router/route-trie-builder.ts +9 -0
  247. package/src/router/router-context.ts +1 -0
  248. package/src/router/router-interfaces.ts +35 -1
  249. package/src/router/router-options.ts +45 -5
  250. package/src/router/segment-resolution/fresh.ts +20 -19
  251. package/src/router/segment-resolution/loader-cache.ts +110 -77
  252. package/src/router/segment-resolution/revalidation.ts +34 -24
  253. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  254. package/src/router/trie-matching.ts +3 -0
  255. package/src/router-versions.ts +60 -0
  256. package/src/router.ts +107 -1
  257. package/src/rsc/full-payload.ts +6 -2
  258. package/src/rsc/handler-context.ts +5 -0
  259. package/src/rsc/handler.ts +64 -7
  260. package/src/rsc/progressive-enhancement.ts +5 -1
  261. package/src/rsc/response-cache-serve.ts +10 -4
  262. package/src/rsc/rsc-rendering.ts +65 -12
  263. package/src/rsc/shell-build-manifest.ts +17 -12
  264. package/src/rsc/shell-capture-constants.ts +39 -7
  265. package/src/rsc/shell-capture.ts +144 -45
  266. package/src/rsc/shell-serve.ts +12 -6
  267. package/src/rsc/ssr-module-loader.ts +26 -0
  268. package/src/rsc/types.ts +12 -6
  269. package/src/segment-boundary-content.ts +25 -0
  270. package/src/segment-loader-promise.ts +28 -38
  271. package/src/segment-system.tsx +117 -57
  272. package/src/server/build-version-table.ts +130 -0
  273. package/src/server/context.ts +40 -17
  274. package/src/server/cookie-store.ts +5 -0
  275. package/src/server/handle-store.ts +357 -127
  276. package/src/server/request-context.ts +149 -12
  277. package/src/ssr/ssr-root.tsx +32 -13
  278. package/src/suspense-audit-react.tsx +98 -0
  279. package/src/suspense-audit.ts +1030 -0
  280. package/src/testing/action-form.ts +66 -0
  281. package/src/testing/build-versions.ts +64 -0
  282. package/src/testing/dispatch.ts +6 -0
  283. package/src/testing/dom.entry.ts +1 -0
  284. package/src/testing/flight-runtime.d.ts +27 -0
  285. package/src/testing/flight.entry.ts +4 -0
  286. package/src/testing/index.ts +22 -0
  287. package/src/testing/internal/context.ts +2 -1
  288. package/src/testing/internal/seed-vars.ts +14 -2
  289. package/src/testing/internal/server-modules.ts +32 -0
  290. package/src/testing/location-state-key.ts +18 -8
  291. package/src/testing/render-handler.ts +2 -1
  292. package/src/testing/render-route.tsx +293 -46
  293. package/src/testing/serve-shell-request.ts +262 -37
  294. package/src/testing/shell-status.ts +17 -6
  295. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  296. package/src/testing/vitest-stubs/version.ts +4 -1
  297. package/src/thenable-status.ts +10 -0
  298. package/src/types/handler-context.ts +15 -5
  299. package/src/types/route-entry.ts +6 -0
  300. package/src/urls/path-helper.ts +69 -9
  301. package/src/use-loader.tsx +57 -30
  302. package/src/vite/discovery/build-versions.ts +1281 -0
  303. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  304. package/src/vite/discovery/discover-routers.ts +15 -0
  305. package/src/vite/discovery/installed-packages.ts +159 -0
  306. package/src/vite/discovery/prerender-collection.ts +38 -1
  307. package/src/vite/discovery/router-versions-phase.ts +336 -0
  308. package/src/vite/discovery/shell-prerender-phase.ts +33 -43
  309. package/src/vite/discovery/state.ts +36 -1
  310. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  311. package/src/vite/encryption-key.ts +101 -9
  312. package/src/vite/inject-client-debug.ts +42 -20
  313. package/src/vite/plugin-types.ts +28 -0
  314. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  315. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  316. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  317. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  318. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  319. package/src/vite/plugins/version-plugin.ts +19 -24
  320. package/src/vite/plugins/version.d.ts +17 -2
  321. package/src/vite/plugins/virtual-entries.ts +39 -6
  322. package/src/vite/rango.ts +28 -3
  323. package/src/vite/router-discovery.ts +96 -13
  324. package/src/vite/utils/bundle-analysis.ts +39 -7
  325. package/src/vite/utils/client-chunks.ts +5 -1
  326. package/src/vite/utils/compare-strings.ts +9 -0
  327. package/src/vite/utils/package-resolution.ts +9 -0
  328. package/src/vite/utils/prerender-utils.ts +4 -1
  329. package/src/vite/utils/shared-utils.ts +1 -1
  330. package/dist/types/segment-content-promise.d.ts +0 -13
  331. package/src/segment-content-promise.ts +0 -67
@@ -14,6 +14,8 @@ The one stub is the HTML step (the SSR module), because `react-dom/server` does
14
14
 
15
15
  So `prelude` is the shell as captured (Flight text, not HTML): a value the shell froze appears there, and a hole (a live loader under `loading()`) does not.
16
16
 
17
+ A `Prerender` route is served as in production: from the artifact `router.matchForPrerender` bakes for the URL (on its first request, kept until `resetShellTestState()`), through the production prerender store, so its handler runs once, at the bake, and never on a request. Its first `ppr` request is a MISS with a runtime capture, as a URL without a build-time shell is in production. The shell `vite build` bakes (served first in production for a URL without a query string) is not reproduced: keep that in e2e. The `env` option doubles as the bake's `buildEnv`. An on-demand route (`Prerender(..., { onDemand })`) is baked only when it is wrapped in `Passthrough()` and its `getParams()` lists the param, as a build bakes it; a plain on-demand route is not baked here, so its pages come from the prerender store alone (`router.prerender()`), and "a removed page the build baked" is a case for a `Passthrough` route or for e2e.
18
+
17
19
  ## API
18
20
 
19
21
  ### Options — `ServeShellRequestOptions`
@@ -27,20 +29,21 @@ So `prelude` is the shell as captured (Flight text, not HTML): a value the shell
27
29
 
28
30
  ### Returns — `ServeShellRequestResult`
29
31
 
30
- | Field | Type | Meaning |
31
- | -------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
32
- | `shellStatus` | `"HIT" \| "MISS" \| null` | `x-rango-shell`; null when the serve path did not consider the request (not a `ppr` route, a nonce'd request, a partial request). |
33
- | `replayStatus` | `PprReplayStatus \| null` | `x-rango-ppr-replay` on a partial request: `{ outcome: "HIT", freshness }` or `{ outcome: "BYPASS", reason }`. |
34
- | `prelude` | `string \| undefined` | The prelude a HIT served (the capture's Flight text). `undefined` unless `shellStatus` is `"HIT"`. |
35
- | `flight` | `string \| undefined` | The Flight payload this request rendered: a HIT's tail, a document render, or a partial response. `undefined` when no Flight rendered (a redirect, a middleware response). |
36
- | `key` | `string` | The shell key the serve path resolved for a document it read (MISS or HIT), request partition included. Otherwise (no `ppr`, a partial request) the URL's key without a partition. |
37
- | `readEntry` | `() => Promise<ShellCacheEntry \| null>` | Reads the document entry under `key` from the request's store (a passive `getShell`). A read: on a store with a shell memo it warms the memo, so call it after the reads you count. |
38
- | `response` | `Response` | Status and headers. Its body is already read. |
39
- | `body` | `string` | The body text. |
32
+ | Field | Type | Meaning |
33
+ | -------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | `shellStatus` | `"HIT" \| "MISS" \| null` | `x-rango-shell`; null when the serve path did not consider the request (not a `ppr` route, a nonce'd request, a partial request). |
35
+ | `replayStatus` | `PprReplayStatus \| null` | `x-rango-ppr-replay` on a partial request: `{ outcome: "HIT", freshness }` or `{ outcome: "BYPASS", reason }`. |
36
+ | `prelude` | `string \| undefined` | The prelude a HIT served (the capture's Flight text). `undefined` unless `shellStatus` is `"HIT"`. |
37
+ | `flight` | `string \| undefined` | The Flight payload this request rendered: a HIT's tail, a document render, or a partial response. `undefined` when no Flight rendered (a redirect, a middleware response). |
38
+ | `key` | `string` | The shell key the serve path resolved for a document it read (MISS or HIT), request partition included. Otherwise (no `ppr`, a partial request) the URL's key without a partition. |
39
+ | `readEntry` | `() => Promise<ShellCacheEntry \| null>` | Reads the document entry under `key` from the request's store (a passive `getShell`). A read: on a store with a shell memo it warms the memo, so call it after the reads you count. |
40
+ | `readHandles` | `() => Promise<ShellRequestHandles \| undefined>` | Decodes the response's handle data as the browser reads it: `{ hydration, late }` (see "Handles and loader data"). `undefined` when no Flight rendered. |
41
+ | `response` | `Response` | Status and headers. Its body is already read. |
42
+ | `body` | `string` | The body text. |
40
43
 
41
44
  ### `resetShellTestState(): Promise<void>`
42
45
 
43
- A worker keeps some PPR state across requests, and so does the test process across tests: the capture's stampede guard and backoff (a refused capture backs its URL off for later tests too), the capture's and the serve path's once-per-key warnings, the build-shell manifest memo, and `CFCacheStore`'s isolate memos (shells, tag markers, tag hints), which every `CFCacheStore` shares by namespace and URL — a later test's first request can be a HIT from an earlier test's shell. `resetShellTestState()` clears all of it. Call it in `beforeEach`, never while a request is in flight. `VercelCacheStore`'s memos live on the `cache` handle you pass it: a new handle per test starts empty.
46
+ A worker keeps some PPR state across requests, and so does the test process across tests: the capture's stampede guard and backoff (a refused capture backs its URL off for later tests too), the capture's and the serve path's once-per-key warnings, the build-shell manifest memo, the `Prerender` artifacts baked so far, and `CFCacheStore`'s isolate memos (shells, tag markers, tag hints), which every `CFCacheStore` shares by namespace and URL — a later test's first request can be a HIT from an earlier test's shell. `resetShellTestState()` clears all of it. Call it in `beforeEach`, never while a request is in flight. `VercelCacheStore`'s memos live on the `cache` handle you pass it: a new handle per test starts empty.
44
47
 
45
48
  ## Recipe
46
49
 
@@ -99,6 +102,35 @@ it("replays the captured segments on a client navigation", async () => {
99
102
 
100
103
  A store built per request from a function `cache` config gets the request's execution context (`ctx`), so its own `waitUntil` writes (`CFCacheStore({ ctx })`) settle with the call. A store built outside the request with its own `waitUntil` (`VercelCacheStore`'s option) is not settled by the call. For `CFCacheStore`, stub the `caches` global and pass a KV double in `env`; for `VercelCacheStore`, pass a `cache` double. A store's shell memo then behaves as in production (count your double's reads across two HITs).
101
104
 
105
+ ## Simulating a deploy: setBuildVersions
106
+
107
+ A stored shell is stamped with the router's document version and a HIT requires the stamp to match. In a build, `vite build` computes that version per `createRouter()`; under test there is no build, so the primitives run unversioned. `setBuildVersions` (from `@rangojs/router/testing`) installs the versions a build would ship, and calling it again with a different `document` is a deploy that changed the router:
108
+
109
+ ```ts
110
+ import { setBuildVersions } from "@rangojs/router/testing";
111
+
112
+ afterEach(() => setBuildVersions()); // remove the versions: unversioned again
113
+
114
+ it("a deploy that changes the document version retires the shell", async () => {
115
+ const cacheStore = new MemorySegmentCacheStore();
116
+ setBuildVersions({ data: "d1", document: "h1" });
117
+
118
+ expect(
119
+ (await serveShellRequest(router, "/product/4", { cacheStore })).shellStatus,
120
+ ).toBe("MISS");
121
+ expect(
122
+ (await serveShellRequest(router, "/product/4", { cacheStore })).shellStatus,
123
+ ).toBe("HIT");
124
+
125
+ setBuildVersions({ data: "d1", document: "h2" }); // new SSR output or client assets
126
+ expect(
127
+ (await serveShellRequest(router, "/product/4", { cacheStore })).shellStatus,
128
+ ).toBe("MISS");
129
+ });
130
+ ```
131
+
132
+ `setBuildVersions({ data, document, routers?: { [routerId]: { data, document } } })` takes the whole-build pair and, optionally, a pair per router id (a router with no entry gets the whole-build pair). `setBuildVersions()` with no argument removes them. The types `BuildVersions` and `RouterVersions` are exported too. `dispatch` sees the same versions.
133
+
102
134
  ## A HIT runs no handler
103
135
 
104
136
  A HIT replays what the capture's handlers produced — their elements, the promises they passed or pushed, their handle values — and runs only middleware and loaders. Count runs to pin it:
@@ -134,9 +166,12 @@ const silver = await serveShellRequest(router, "/pricing", tier("silver"));
134
166
  expect(silver.shellStatus).toBe("MISS"); // its own capture, never gold's shell
135
167
  const gold = await serveShellRequest(router, "/pricing", tier("gold"));
136
168
  expect(gold.shellStatus).toBe("HIT");
137
- expect(gold.key).toBe(shellCacheKey("/pricing", undefined, "tier:gold"));
138
- // "localhost/pricing:shell|key%3Atier%253Agold": the key() result is
139
- // namespaced as production stores it (issue #975).
169
+ expect(gold.key).toBe(
170
+ shellCacheKey(router, "/pricing", undefined, "tier:gold"),
171
+ );
172
+ // "{router.id}@localhost/pricing:shell|key%3Atier%253Agold": the router the
173
+ // shell belongs to, then the key() result namespaced as production stores it
174
+ // (issue #975).
140
175
  expect(await gold.readEntry()).not.toBeNull();
141
176
  ```
142
177
 
@@ -149,7 +184,7 @@ Under nested keyed `cache()` boundaries the partition is their `key()` results c
149
184
  // ]),
150
185
  // ])
151
186
  expect(result.key).toBe(
152
- shellCacheKey("/plans", undefined, ["tier:gold", "v:a"]),
187
+ shellCacheKey(router, "/plans", undefined, ["tier:gold", "v:a"]),
153
188
  );
154
189
  ```
155
190
 
@@ -159,24 +194,134 @@ A store `keyGenerator` result that partitions the record partitions the shell to
159
194
  // createRouter({ cache: { store: localeStore } }), where localeStore's
160
195
  // keyGenerator returns `${defaultKey}|${locale}`
161
196
  expect(result.key).toBe(
162
- shellCacheKey("/pricing", undefined, {
163
- generated: ["doc:localhost/pricing|de"],
197
+ shellCacheKey(router, "/pricing", undefined, {
198
+ generated: [`doc:${router.id}@localhost/pricing|de`],
164
199
  }),
165
200
  );
166
201
  ```
167
202
 
168
203
  A partial request has no HTML step, so its `key` is the URL's key without a partition: read a partitioned route's entry from a document request's result.
169
204
 
205
+ ## Two routers on one cache store
206
+
207
+ A shell's key starts with its router's id (`shellCacheKey(router, url)`), so two routers that share a store and serve the same host and path each read and write their own (why and when: `/host-router`, "Shared cache store"). Serve both against one store to pin it:
208
+
209
+ ```ts
210
+ const store = new MemorySegmentCacheStore();
211
+ // appA and appB: createRouter({ id: "app-a" | "app-b", cache: { store } }),
212
+ // each with path("/pricing", ..., { ppr: true })
213
+ const url = "http://preview.dev/pricing";
214
+
215
+ await serveShellRequest(appA, url); // MISS + capture
216
+ const fromB = await serveShellRequest(appB, url);
217
+ expect(fromB.shellStatus).toBe("MISS"); // never app A's shell
218
+ expect((await serveShellRequest(appA, url)).shellStatus).toBe("HIT");
219
+ expect((await serveShellRequest(appB, url)).shellStatus).toBe("HIT");
220
+ ```
221
+
222
+ Vitest runs no Vite id transform, so give each router an explicit `id` when a test builds more than one, and do not reuse one `id` for routers with different routes in the same file: the route manifest is registered per id.
223
+
170
224
  ## Handles and loader data
171
225
 
172
- The result carries no structured `handles` map (unlike `renderHandler`): a handle value and loader data ride the payload, so you assert them as text in `prelude` and `flight`. A value the handler pushed is in the prelude and in every HIT's tail; a live loader's data is only in the tail. No public helper decodes a payload string into values: `normalizeFlight` and the `flightMatchers` (`toMatchFlight`) work on the text too.
226
+ `readHandles()` decodes the handle data the response carries, the way the browser reads it, with deferred values resolved. Each field is the raw handle data (`{ [handleId]: { [segmentId]: values[] } }`):
227
+
228
+ | Field | What it is |
229
+ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
230
+ | `hydration` | What the document hydrates with (`metadata.handles`, read to its end before hydration starts). For a `partial` request, the last state it streamed. |
231
+ | `late` | The states that arrived on the late channel (`metadata.handlesLate`), in order. Each replaces the client's handle data after hydration. Often `[]`. |
232
+
233
+ On a HIT the client hydrates with exactly what the shell was rendered from, so `hydration` is also what the shell's HTML holds, and whatever this request's loaders push, replace or drop is in `late` (see `/ppr`, "Handles on a shell HIT"):
173
234
 
174
235
  ```ts
175
- // The handler pushed ctx.use(Meta)({ title: "Widget - Shop" })
176
- expect(hit.prelude).toContain('"title":"Widget - Shop"');
177
- expect(hit.flight).toContain('"title":"Widget - Shop"');
236
+ // The handler pushed ctx.use(Notes)("from-handler"); a live loader pushes
237
+ // ctx.use(Notes)("in stock").
238
+ const values = (data) =>
239
+ Object.values(data ?? {}).flatMap((s) => Object.values(s).flat());
240
+
241
+ const hit = await serveShellRequest(router, "/product/1", { cacheStore });
242
+ const handles = await hit.readHandles();
243
+ expect(values(handles.hydration)).toEqual(["from-handler"]); // the shell's HTML, hydrates clean
244
+ expect(values(handles.late.at(-1))).toEqual(["from-handler", "in stock"]); // after hydration
178
245
  ```
179
246
 
247
+ Loader data has no decoder: it rides the payload, so assert it as text in `prelude` and `flight`. `normalizeFlight` and the `flightMatchers` (`toMatchFlight`) work on the text too. A live loader's data is only in the tail.
248
+
249
+ ```ts
250
+ expect(hit.flight).toContain('"stock":"in stock"');
251
+ ```
252
+
253
+ ## Warming a route: router.prerender()
254
+
255
+ `router.prerender()` warms a route that is not `Prerender(..., { onDemand })`
256
+ through `router.fetch` (`/prerender` → "Warm any route before traffic").
257
+ Importing `serveShellRequest` gives that handler the same HTML stub, so the
258
+ whole path runs here: warm, then serve the next visitor's request.
259
+
260
+ ```tsx
261
+ class SharedMemoryStore extends MemorySegmentCacheStore {
262
+ // The shipped store is "local" and refused; a test store says it is shared.
263
+ readonly scope = "global" as const;
264
+ }
265
+
266
+ const router = createRouter({
267
+ cache: { store: new SharedMemoryStore() },
268
+ }).routes(
269
+ urls(({ path }) => [path("/product/:id", ProductPage, { ppr: true })]),
270
+ );
271
+
272
+ const result = await router.prerender({ env: {} })(
273
+ "http://localhost/product/1",
274
+ );
275
+ expect(result).toMatchObject({ ok: true, path: "warm", status: "warmed" });
276
+
277
+ // No visitor has requested the URL: its first document is a HIT.
278
+ const hit = await serveShellRequest(router, "/product/1");
279
+ expect(hit.shellStatus).toBe("HIT");
280
+ ```
281
+
282
+ - Configure the store on the router. A warm writes to the store
283
+ `createRouter({ cache })` resolves, not to the `cacheStore` option of
284
+ `serveShellRequest`.
285
+ - Give the target the origin `serveShellRequest` uses (`http://localhost`), as
286
+ a full URL or with `router.prerender({ env, origin })`: cache keys carry the
287
+ host. A path target with neither returns `skipped-no-origin`, unless the
288
+ runner is called from inside a served request (a route handler that calls
289
+ `router.prerender()`), where it takes that request's origin.
290
+ - To assert a replace, count handler runs or render a per-run stamp: fill the
291
+ cache with one request, move the data, warm, then check the next request
292
+ shows the new value and ran no handler.
293
+ - `router.fetch` binds its handler's document version once, as a production
294
+ isolate does, so `setBuildVersions()` between two warms does not move it.
295
+
296
+ ## A form submitted before hydration
297
+
298
+ `serveShellRequest(router, url, { form })` POSTs a form the way a browser does before the page hydrates, through the production no-JS path: the action runs, the page re-renders, and for a `useActionState` form the document payload carries the form state. Build the body with `createActionForm(action, { id })` from the same entry; it writes React's hidden fields and registers `action` under `id` (`<module id>#<export name>`, any unique string of that shape in a test).
299
+
300
+ ```tsx
301
+ import {
302
+ createActionForm,
303
+ serveShellRequest,
304
+ } from "@rangojs/router/testing/flight";
305
+
306
+ const form = createActionForm(makeToken, { id: "actions/token#makeToken" });
307
+ const result = await serveShellRequest(router, "/token", { form });
308
+
309
+ // What the SSR render received, and what the browser entry hands hydrateRoot:
310
+ expect(result.formState).toEqual([
311
+ { token: "tok-1" },
312
+ "k0",
313
+ "actions/token#makeToken",
314
+ 0,
315
+ ]);
316
+ expect(await result.readPayloadFormState()).toEqual(result.formState);
317
+ ```
318
+
319
+ - `useActionState: false` builds a plain `<form action={fn}>` POST (`$ACTION_ID_`): the action runs as `(formData)` and no form state is carried. Default true runs `(prevState, formData)` with the hook's initial `null`. `fields` are the form's own fields (`formData.get("name")`).
320
+ - An action returning a state Flight cannot serialize (a class instance) still renders the document with status 200 and `formState` holds the raw value, but `readPayloadFormState()` rejects: the entry catches that and hydrates without it, as before the fix.
321
+ - A GET, a plain-form POST and a redirecting action carry no form state: `formState` and `readPayloadFormState()` are undefined, a redirect has no `flight`.
322
+ - Request headers come from `headers`. No `Origin` is sent by default, which the origin guard allows.
323
+ - Not with `partial`. The hydration itself (`hydrateRoot` taking the state, the hook not resetting) is e2e.
324
+
180
325
  ## Caveats
181
326
 
182
327
  What the HTML stub cannot reproduce, so keep it in e2e:
@@ -192,7 +337,7 @@ Other caveats:
192
337
  - Call `resetShellTestState()` in `beforeEach` (see above). Without it, a URL a previous test left backed off, or a shell memoized by another `CFCacheStore`, changes what the next test sees.
193
338
  - `readEntry()` reads the document entry under `key`. A partial request that finds no entry captures a navigation-only entry, which is stored apart from it.
194
339
  - `updateTag()` needs a request context with the store: call it through `runInRequestContext(fn, { cacheStore })` or your app's own endpoint (`dispatch`). A store refuses a capture that starts in the invalidation's millisecond (the invalidation wins) and backs the key off; `serveShellRequest` starts each request in a later millisecond than the call, so an invalidation made before the call cannot collide with it.
195
- - GET only. Server actions are not served; test their effects with `runInRequestContext`.
340
+ - GET, and one POST shape: a form submitted before hydration (see "A form submitted before hydration" above). Other server action requests are not served; test their effects with `runInRequestContext`.
196
341
  - Same setup as every Flight test: the react-server condition and `rangoTestAliases()` (see [`./setup.md`](./setup.md)).
197
342
 
198
343
  ## See also
@@ -8,22 +8,22 @@
8
8
 
9
9
  ### Options — `CreateTestContextOptions<TEnv>`
10
10
 
11
- | Field | Type | Meaning |
12
- | --------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
- | `env` | `TEnv` | Platform bindings the action reads (`ctx.env`). Default `{}`. Double them yourself (see `./bindings.md`). |
14
- | `request` | `Request \| string` | The request to run under. A `string` becomes `new Request(url)`; pass a full `Request` to seed a `Cookie` header. Default origin `http://localhost/`. |
15
- | `requestInit` | `RequestInit` | Init merged when `request` is a string (e.g. `{ method, headers, body }`). |
16
- | `variables` | `Record<string, unknown>` | Raw backing store for `ctx.get()` / `ctx.set()`, pre-seeded from `vars`. |
17
- | `vars` | `VarsInit` | Vars a prior middleware would have set (object or `[token, value]` list). |
18
- | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()` without global state. |
19
- | `routeName` | `string` | Current route name (drives `ctx.reverse()` self-references). |
20
- | `params` | `Record<string, string>` | Route params on `ctx.params`. |
21
- | `basename` | `string` | Router basename, normalized exactly like `createRouter({ basename })`; drives `redirect()` prefixing. Default `undefined`. |
22
- | `cacheStore` | `SegmentCacheStore` | Backing store for `use cache` functions (same shape as `createRouter({ cache })`). Without it, cached functions run uncached and their guards never fire. |
23
- | `cacheProfiles` | `Record<string, CacheProfile>` | Profiles for `use cache: "name"`, same shape as `createRouter({ cacheProfiles })`. An unknown profile throws. |
24
- | `theme` | `ThemeConfig \| true` | Theme config (same shape as `createRouter({ theme })`). Without it `ctx.theme` / `ctx.setTheme` are inert. |
25
- | `build` | `boolean` | Seed `ctx.build` (default `false`) for code that branches on the build-time PPR shell-capture pass. With `true`, `ctx.waitUntil()` is inert, as at build time. |
26
- | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie an action calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production; override `prefix`/`routerId` to match `createRouter({ stateCookiePrefix, id })`, or `version` (value is `{version}:{timestamp}`, default `"0"`). |
11
+ | Field | Type | Meaning |
12
+ | --------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `env` | `TEnv` | Platform bindings the action reads (`ctx.env`). Default `{}`. Double them yourself (see `./bindings.md`). |
14
+ | `request` | `Request \| string` | The request to run under. A `string` becomes `new Request(url)`; pass a full `Request` to seed a `Cookie` header. Default origin `http://localhost/`. |
15
+ | `requestInit` | `RequestInit` | Init merged when `request` is a string (e.g. `{ method, headers, body }`). |
16
+ | `variables` | `Record<string, unknown>` | Raw backing store for `ctx.get()` / `ctx.set()`, pre-seeded from `vars`. |
17
+ | `vars` | `VarsInit` | Vars a prior middleware would have set (object or `[token, value]` list). |
18
+ | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()` without global state. |
19
+ | `routeName` | `string` | Current route name (drives `ctx.reverse()` self-references). |
20
+ | `params` | `Record<string, string>` | Route params on `ctx.params`. |
21
+ | `basename` | `string` | Router basename, normalized exactly like `createRouter({ basename })`; drives `redirect()` prefixing. Default `undefined`. |
22
+ | `cacheStore` | `SegmentCacheStore` | Backing store for `use cache` functions (same shape as `createRouter({ cache })`). Without it, cached functions run uncached and their guards never fire. |
23
+ | `cacheProfiles` | `Record<string, CacheProfile>` | Profiles for `use cache: "name"`, same shape as `createRouter({ cacheProfiles })`. An unknown profile throws. |
24
+ | `theme` | `ThemeConfig \| true` | Theme config (same shape as `createRouter({ theme })`). Without it `ctx.theme` / `ctx.setTheme` are inert. |
25
+ | `build` | `boolean` | Seed `ctx.build` (default `false`) for code that branches on the build-time PPR shell-capture pass. With `true`, `ctx.waitUntil()` is inert, as at build time. |
26
+ | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie an action calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production; override `prefix`/`routerId` to match `createRouter({ stateCookiePrefix, id })`, or `version` (the app's version, as `createRouter({ version })` would set it; value is `{version}:{timestamp}`, default `"0"`). |
27
27
 
28
28
  ### Context — `RequestContext<TEnv>` (what your code receives)
29
29
 
@@ -110,7 +110,7 @@ it("asserts the client-cache directives an action issued", async () => {
110
110
  ## Caveats
111
111
 
112
112
  - The snapshot fires whether `fn` RETURNS or THROWS. A `throw redirect("/app")` on the success path is captured on `thrown` (NOT re-thrown), so no try/catch is needed; assert on `thrown` for a throwing action.
113
- - Location state needs a key. A `createLocationState()` definition gets it from the Vite plugin, which a unit-test project does not run, and outside production an unkeyed `Flash(value)` throws before `redirect()` runs: `thrown` is that missing-key error, not the redirect. Call `withLocationStateKey(Flash, "Flash")` once per definition; `locationState` then holds `{ __rsc_ls_Flash: value }` (the helper adds the `__rsc_ls_` prefix; `{ [Flash.__rsc_ls_key]: value }` works for any key).
113
+ - Location state needs a key. A `createLocationState()` definition gets it from the Vite plugin, which a unit-test project does not run, and outside production an unkeyed `Flash(value)` throws before `redirect()` runs: `thrown` is that missing-key error, not the redirect. Call `withLocationStateKey(Flash, "Flash")` once per definition; `locationState` then holds `{ __rsc_ls_Flash: value }` (the helper adds the `__rsc_ls_` prefix; `{ [Flash.__rsc_ls_key]: value }` works for any key, including one that carries the `clearOnReload` suffix, `__rsc_ls_Flash~r`).
114
114
  - There is no cookies / headers option. Seed a request cookie by passing a full `Request` with the `Cookie` header (as in the recipe).
115
115
  - An action has no loader context, so `runLoader` is the wrong shape for it even when the action reads cookies or vars; use `runInRequestContext`.
116
116
  - Platform bindings are yours to double via `env` (see `./bindings.md`).
@@ -128,7 +128,7 @@ it("asserts the server-rendered host content", async () => {
128
128
  - A client boundary's props come back as REAL JS values after deserialization (a `Date` is a `Date`, not a `$D...` encoding) — but there is NO hydration and NO interaction; boundaries are inert placeholders carrying props.
129
129
  - Server COMPONENTS do not survive Flight as identities (they are executed during serialization), so `findElements` matches the host elements they PRODUCED, not the component function. Client islands keep identity — use `findClientBoundaries` for those.
130
130
  - `findClientBoundaries` finds islands (`I` rows); `findElements` finds host elements. A `testId` on an island matches with `findClientBoundaries`; a `testId` on a host element matches with `findElements`. Use `textContent(node)` in place of `JSON.stringify(tree).toContain`.
131
- - A true interactive, clickable DOM `renderServer` is intentionally NOT shipped: in-process happy-dom hydration re-tests React more than your app and misses server/client divergence (the only hydration bug worth a dedicated test, which needs a real browser). Test interaction at e2e.
131
+ - A true interactive, clickable DOM `renderServer` is intentionally NOT shipped: in-process happy-dom hydration of a Flight tree re-tests React more than your app and misses the divergence that needs the real document (streaming order, client-reference identity, the browser's HTML parser). Test interaction at e2e. A CLIENT component's own server/client divergence is unit-reachable: `renderRoute` with `hydrate: true` ([`./client-components.md`](./client-components.md#hydration)).
132
132
 
133
133
  ## See also
134
134
 
@@ -113,7 +113,7 @@ and client helpers.
113
113
  CLI flags: the default mode fails (and writes nothing) when an `include()` cannot
114
114
  be resolved statically (factory functions, dynamic expressions); `--static`
115
115
  accepts partial output with warnings; `--runtime` uses Vite-based discovery for
116
- full coverage (needs `vite` and `@vitejs/plugin-rsc`, optional
116
+ full coverage (needs `vite`, optional
117
117
  `--config <path>`). Passing a directory scans it for router and `urls()` files.
118
118
 
119
119
  ### Named Routes, `$$routeNames`, And `router.routeMap`
@@ -151,11 +151,15 @@ argument. The same holds for a loader's own `cache()` (`/loader` → "Cache Key"
151
151
 
152
152
  The tainted `ctx` object is excluded from arg serialization (see below), but
153
153
  route-identifying fields read off it are extracted into `serializedArgs`:
154
- `url.host`, route name (`_routeName`), `pathname`, `params`, response type
155
- (`_responseType`), and the user-facing sorted search params (internal `_rsc*`/`__`
156
- params excluded). The same cached function called with `ctx` on different routes,
157
- param combinations, hosts, response types, or query variants therefore produces
158
- distinct cache entries -- not one shared entry.
154
+ the serving router's id and `url.host` (`{routerId}@{host}`), route name
155
+ (`_routeName`), `pathname`, `params`, response type (`_responseType`), and the
156
+ user-facing sorted search params (internal `_rsc*`/`__` params excluded). The
157
+ same cached function called with `ctx` on different routes, param combinations,
158
+ hosts, response types, or query variants therefore produces distinct cache
159
+ entries -- not one shared entry. Two routers that call it on one host and path
160
+ keep two entries as well: a `ctx` is one router's (`/host-router`, "Shared
161
+ cache store"). A bare `Request` argument folds in its URL only, so it names no
162
+ router.
159
163
 
160
164
  ## Request-Scoped Arguments (ctx, Request, env)
161
165
 
@@ -485,11 +489,22 @@ the current time and expect freshness -- those are frozen at cache-write. The
485
489
  `cookies()`/`headers()` read guard applies to the cached function body, NOT to an
486
490
  inline action's body.
487
491
 
488
- Deploy note: captured values are encrypted with a per-build key by default. If
489
- cache entries can outlive a deploy (a persistent store such as the Cloudflare
490
- cache store), set `RANGO_ENCRYPTION_KEY` (base64-encoded 32 bytes) in the build
491
- environment so actions embedded in entries written by a previous deploy still
492
- decrypt; without it they fail until the entry expires or revalidates.
492
+ Deploy note: captured values are encrypted with a key that is generated per
493
+ build by default. Set a stable one in the plugin options:
494
+
495
+ ```ts
496
+ rango({ encryptionKey: process.env.RANGO_ENCRYPTION_KEY });
497
+ ```
498
+
499
+ The value is base64-encoded 32 bytes (`openssl rand -base64 32`), never a
500
+ literal, and is validated when `rango()` is called. `undefined` falls back to the
501
+ `RANGO_ENCRYPTION_KEY` environment variable, then to a key generated per build.
502
+ The key is part of the cache version of every router whose server code encrypts
503
+ inline server-action bound arguments, so without a stable key those routers get a
504
+ new version (and a cleared cache) on every build, and the build prints one note
505
+ when that applies. With a stable key, actions embedded in entries written by a
506
+ previous deploy still decrypt. A router that renders no inline action with bound
507
+ arguments keeps its cache without a stable key. See `/cache-guide`.
493
508
 
494
509
  ## Vite Transform
495
510
 
@@ -98,9 +98,7 @@ const memoryStore = new MemorySegmentCacheStore({ defaults });
98
98
  // passes, since the tag markers are regional and only expireTag is global. A
99
99
  // platform expireTag issued outside rango writes no marker, so every memo
100
100
  // serves the shell until its window passes.
101
- const runtimeCache = process.env.VERCEL
102
- ? getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })
103
- : undefined;
101
+ const runtimeCache = process.env.VERCEL ? getCache() : undefined;
104
102
 
105
103
  function resolveCache() {
106
104
  if (runtimeCache) {
@@ -120,6 +118,10 @@ export const router = createRouter({ cache: resolveCache }).routes(/* ... */);
120
118
 
121
119
  The cache factory receives `(env, ctx)`: on Vercel `env` is `process.env` and `ctx` is `{ waitUntil }`; off-platform `ctx` is `undefined`. Cache semantics and tag invalidation: `/caching`, `/cache-guide`.
122
120
 
121
+ `router.prerender()` can warm a route into this store before traffic (`/prerender` → "Warm any route before traffic"). The Runtime Cache is per region, so `VercelCacheStore` declares `scope: "regional"`: a warm fills the region the call runs in. That is all traffic when the project's functions run in one region (the default `iad1` for new projects, and every Hobby project); on a multi-region project it warms one region and the others fill on first use. The router does not detect the region count. The memory store used off-platform above is refused in production and allowed under the dev server.
122
+
123
+ `getCache()` takes no deployment id: `VercelCacheStore` puts a version in front of every key itself (data version for families `s`, `i`; document version for `r`, `h`; none for tag markers), computed per `createRouter()` from its built code. A deploy that does not change a router keeps its entries; a deploy that changes it reads under new keys. `version` on the store (or on `createRouter`) replaces both versions. To clear the cache on every deploy, set `version` to a per-deploy value or keep a deployment-scoped `getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`. Two environments built from the same code share entries if they share a cache namespace; give each its own namespace or `version`.
124
+
123
125
  ## Host routers (multi-app)
124
126
 
125
127
  A multi-app host router deploys as a **single function** running `hostRouter.match()` for every request (mirrors the Cloudflare single-worker model). Two requirements:
@@ -359,10 +359,15 @@ interface TransitionWhenContext {
359
359
  the route or loses its state
360
360
  ([#995](https://github.com/rangojs/rango/issues/995)).
361
361
  - That holds for every kind, actions and revalidations included: a gated-off
362
- action or revalidation commit is urgent too, so a segment it re-renders
363
- streams its `loading()`. The router commits an action's result after the
364
- response arrives, outside the transition React opens for a form action or
365
- `useActionState` call, so React does not hold it either.
362
+ action or revalidation commit is urgent and unanimated too, and a segment it
363
+ re-renders shows its `loading()` only while something in it is still pending
364
+ (a gated-off navigation whose data is still streaming shows it; an action
365
+ whose response already carries the data does not in a production build,
366
+ while the dev server still shows it briefly because it resolves each
367
+ response's client components asynchronously). The router commits an
368
+ action's result after the response arrives, outside the transition React
369
+ opens for a form action or `useActionState` call, so React does not hold it
370
+ either.
366
371
  - A predicate that throws counts as `false` and is logged with
367
372
  `console.error` (the conservative default: no hold).
368
373
  - Intercept (modal) commits skip the evaluation, including a back/forward
package/src/bin/rango.ts CHANGED
@@ -76,7 +76,7 @@ if (command === "generate") {
76
76
  Modes:
77
77
  (default) Static parser with error on unresolvable includes
78
78
  --runtime Vite-based runtime discovery (100% coverage)
79
- Requires vite and @vitejs/plugin-rsc
79
+ Requires vite
80
80
  --static Static parser, accept partial output with warnings
81
81
 
82
82
  Options: