@rangojs/router 0.24.4 → 0.26.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 (301) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/rango.js +363 -39
  3. package/dist/testing/vitest.js +37 -4
  4. package/dist/types/browser/event-controller.d.ts +24 -5
  5. package/dist/types/browser/intercept-utils.d.ts +5 -2
  6. package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
  7. package/dist/types/browser/navigation-transaction.d.ts +5 -5
  8. package/dist/types/browser/partial-update.d.ts +21 -13
  9. package/dist/types/browser/prefetch/cache.d.ts +8 -0
  10. package/dist/types/browser/rango-marker.d.ts +7 -0
  11. package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
  12. package/dist/types/browser/react/use-handle.d.ts +20 -0
  13. package/dist/types/browser/react/use-href.d.ts +6 -0
  14. package/dist/types/browser/react/use-router.d.ts +6 -1
  15. package/dist/types/browser/response-adapter.d.ts +5 -3
  16. package/dist/types/browser/scroll-restoration.d.ts +9 -3
  17. package/dist/types/browser/segment-reconciler.d.ts +10 -13
  18. package/dist/types/browser/types.d.ts +38 -8
  19. package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +0 -2
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +0 -16
  22. package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
  23. package/dist/types/cache/handle-snapshot.d.ts +2 -2
  24. package/dist/types/cache/types.d.ts +16 -6
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +1 -15
  26. package/dist/types/client-urls/client-urls.d.ts +6 -2
  27. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  28. package/dist/types/client-urls/server-projection.d.ts +9 -5
  29. package/dist/types/client-urls/types.d.ts +29 -16
  30. package/dist/types/client.d.ts +2 -2
  31. package/dist/types/client.rsc.d.ts +2 -2
  32. package/dist/types/decode-loader-results.d.ts +28 -7
  33. package/dist/types/handles/deferred-resolution.d.ts +3 -2
  34. package/dist/types/handles/meta.d.ts +5 -0
  35. package/dist/types/href-client.d.ts +3 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/loader-redirect.d.ts +2 -3
  38. package/dist/types/outlet-context.d.ts +7 -6
  39. package/dist/types/prefetch-markers.d.ts +14 -5
  40. package/dist/types/prerender/build-shell-capture.d.ts +1 -2
  41. package/dist/types/prerender/writable-store.d.ts +2 -3
  42. package/dist/types/route-content-wrapper.d.ts +17 -14
  43. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  44. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  45. package/dist/types/route-definition/rewrite.d.ts +48 -5
  46. package/dist/types/route-types.d.ts +2 -2
  47. package/dist/types/router/handler-context.d.ts +14 -2
  48. package/dist/types/router/intercept-resolution.d.ts +19 -46
  49. package/dist/types/router/lazy-includes.d.ts +7 -0
  50. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  51. package/dist/types/router/middleware.d.ts +0 -16
  52. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  53. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  54. package/dist/types/router/revalidation.d.ts +8 -1
  55. package/dist/types/router/router-context.d.ts +4 -16
  56. package/dist/types/router/router-interfaces.d.ts +0 -11
  57. package/dist/types/router/router-options.d.ts +4 -27
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +5 -3
  59. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  60. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  61. package/dist/types/router/segment-resolution.d.ts +1 -1
  62. package/dist/types/router/segment-wrappers.d.ts +3 -5
  63. package/dist/types/router/substitute-pattern-params.d.ts +3 -2
  64. package/dist/types/router/tracing.d.ts +2 -2
  65. package/dist/types/router-versions.d.ts +8 -6
  66. package/dist/types/rsc/full-payload.d.ts +14 -2
  67. package/dist/types/rsc/helpers.d.ts +8 -0
  68. package/dist/types/rsc/manifest-init.d.ts +20 -0
  69. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  70. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  71. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  72. package/dist/types/rsc/server-action.d.ts +6 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +14 -4
  74. package/dist/types/rsc/shell-capture.d.ts +23 -2
  75. package/dist/types/rsc/types.d.ts +5 -8
  76. package/dist/types/segment-system.d.ts +15 -0
  77. package/dist/types/server/build-version-table.d.ts +10 -13
  78. package/dist/types/server/context.d.ts +90 -6
  79. package/dist/types/server/handle-store.d.ts +17 -6
  80. package/dist/types/server/request-context.d.ts +45 -7
  81. package/dist/types/server/request-headers.d.ts +12 -0
  82. package/dist/types/server/rewrite-trail.d.ts +8 -1
  83. package/dist/types/testing/build-versions.d.ts +0 -3
  84. package/dist/types/testing/e2e/index.d.ts +1 -1
  85. package/dist/types/testing/flight.entry.d.ts +1 -1
  86. package/dist/types/testing/index.d.ts +1 -1
  87. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  88. package/dist/types/testing/internal/seed-vars.d.ts +4 -4
  89. package/dist/types/testing/render-route.d.ts +21 -11
  90. package/dist/types/testing/run-loader.d.ts +7 -0
  91. package/dist/types/testing/run-middleware.d.ts +7 -0
  92. package/dist/types/testing/run-revalidate.d.ts +5 -4
  93. package/dist/types/testing/serve-shell-request.d.ts +42 -5
  94. package/dist/types/testing/shell-status.d.ts +17 -0
  95. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  96. package/dist/types/testing/vitest.d.ts +10 -1
  97. package/dist/types/thenable-status.d.ts +0 -6
  98. package/dist/types/types/handler-context.d.ts +12 -1
  99. package/dist/types/types/loader-types.d.ts +10 -4
  100. package/dist/types/types/route-entry.d.ts +2 -1
  101. package/dist/types/urls/path-helper-types.d.ts +4 -2
  102. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  103. package/dist/types/vite/discovery/state.d.ts +20 -0
  104. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  105. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  106. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  107. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  108. package/dist/vite/index.js +6819 -6555
  109. package/package.json +1 -1
  110. package/skills/app-structure/SKILL.md +255 -0
  111. package/skills/app-structure/content-site.md +333 -0
  112. package/skills/app-structure/e-commerce.md +435 -0
  113. package/skills/app-structure/large-app.md +885 -0
  114. package/skills/app-structure/multi-app.md +280 -0
  115. package/skills/breadcrumbs/SKILL.md +6 -1
  116. package/skills/cache-guide/SKILL.md +4 -8
  117. package/skills/caching/SKILL.md +6 -6
  118. package/skills/catalog.json +12 -0
  119. package/skills/client-urls/SKILL.md +185 -32
  120. package/skills/comparison/references/framework-comparison.md +1 -1
  121. package/skills/composability/SKILL.md +10 -8
  122. package/skills/handler-use/SKILL.md +48 -9
  123. package/skills/hooks/handle-and-actions.md +104 -0
  124. package/skills/hooks/navigation.md +24 -10
  125. package/skills/hooks/urls.md +1 -1
  126. package/skills/host-router/SKILL.md +1 -1
  127. package/skills/i18n/SKILL.md +2 -1
  128. package/skills/intercept/SKILL.md +60 -29
  129. package/skills/layout/SKILL.md +6 -3
  130. package/skills/links/SKILL.md +151 -17
  131. package/skills/loader/SKILL.md +42 -19
  132. package/skills/middleware/SKILL.md +111 -15
  133. package/skills/observability/SKILL.md +11 -0
  134. package/skills/parallel/SKILL.md +5 -5
  135. package/skills/ppr/SKILL.md +103 -52
  136. package/skills/prerender/SKILL.md +4 -5
  137. package/skills/rango/SKILL.md +3 -0
  138. package/skills/response-routes/SKILL.md +4 -1
  139. package/skills/router-setup/SKILL.md +0 -2
  140. package/skills/scripts/SKILL.md +12 -1
  141. package/skills/server-actions/SKILL.md +60 -20
  142. package/skills/testing/SKILL.md +1 -0
  143. package/skills/testing/client-components.md +13 -10
  144. package/skills/testing/loader.md +25 -24
  145. package/skills/testing/middleware.md +11 -11
  146. package/skills/testing/render-handler.md +17 -17
  147. package/skills/testing/response-routes.md +4 -1
  148. package/skills/testing/serve-shell-request.md +86 -22
  149. package/skills/testing/server-actions.md +16 -16
  150. package/skills/ux-architecture/SKILL.md +980 -0
  151. package/skills/vercel/SKILL.md +1 -1
  152. package/skills/view-transitions/SKILL.md +43 -10
  153. package/src/__augment-tests__/augmented.check.ts +28 -0
  154. package/src/browser/event-controller.ts +44 -18
  155. package/src/browser/intercept-utils.ts +3 -2
  156. package/src/browser/merge-segment-loaders.ts +0 -85
  157. package/src/browser/navigation-bridge.ts +112 -84
  158. package/src/browser/navigation-client.ts +20 -15
  159. package/src/browser/navigation-store.ts +4 -2
  160. package/src/browser/navigation-transaction.ts +9 -8
  161. package/src/browser/partial-update.ts +90 -32
  162. package/src/browser/prefetch/cache.ts +8 -0
  163. package/src/browser/prefetch/fetch.ts +13 -3
  164. package/src/browser/rango-marker.ts +13 -0
  165. package/src/browser/react/NavigationProvider.tsx +25 -7
  166. package/src/browser/react/filter-segment-order.ts +13 -0
  167. package/src/browser/react/use-action.ts +9 -15
  168. package/src/browser/react/use-handle.ts +63 -71
  169. package/src/browser/react/use-href.tsx +24 -5
  170. package/src/browser/react/use-link-status.ts +1 -1
  171. package/src/browser/react/use-navigation.ts +30 -40
  172. package/src/browser/react/use-router.ts +37 -6
  173. package/src/browser/react/use-segments.ts +8 -12
  174. package/src/browser/response-adapter.ts +11 -5
  175. package/src/browser/rsc-router.tsx +3 -1
  176. package/src/browser/scroll-restoration.ts +34 -20
  177. package/src/browser/segment-reconciler.ts +11 -29
  178. package/src/browser/server-action-bridge.ts +52 -16
  179. package/src/browser/types.ts +40 -8
  180. package/src/build/generate-manifest.ts +12 -1
  181. package/src/build/route-types/codegen.ts +6 -2
  182. package/src/build/route-types/per-module-writer.ts +94 -61
  183. package/src/cache/cache-runtime.ts +9 -0
  184. package/src/cache/cache-scope.ts +2 -1
  185. package/src/cache/cf/cf-cache-store.ts +6 -7
  186. package/src/cache/cf/cf-cache-types.ts +0 -17
  187. package/src/cache/cf/cf-shell-frame.ts +3 -0
  188. package/src/cache/handle-snapshot.ts +2 -2
  189. package/src/cache/types.ts +16 -6
  190. package/src/cache/vercel/vercel-cache-store.ts +8 -19
  191. package/src/client-urls/client-urls.ts +202 -153
  192. package/src/client-urls/revalidate-registry.ts +12 -3
  193. package/src/client-urls/server-projection.ts +137 -66
  194. package/src/client-urls/types.ts +30 -15
  195. package/src/client.rsc.tsx +11 -1
  196. package/src/client.tsx +9 -1
  197. package/src/decode-loader-results.ts +64 -31
  198. package/src/handles/deferred-resolution.ts +3 -2
  199. package/src/handles/meta.ts +5 -0
  200. package/src/href-client.ts +3 -1
  201. package/src/index.rsc.ts +1 -0
  202. package/src/index.ts +12 -4
  203. package/src/loader-redirect.tsx +2 -3
  204. package/src/outlet-context.ts +7 -6
  205. package/src/prefetch-markers.ts +15 -5
  206. package/src/prerender/build-shell-capture.ts +1 -2
  207. package/src/prerender/writable-store.ts +2 -3
  208. package/src/reverse.ts +3 -3
  209. package/src/route-content-wrapper.tsx +19 -17
  210. package/src/route-definition/dsl-helpers.ts +41 -21
  211. package/src/route-definition/helpers-types.ts +7 -2
  212. package/src/route-definition/resolve-handler-use.ts +23 -10
  213. package/src/route-definition/rewrite.ts +86 -19
  214. package/src/route-types.ts +1 -2
  215. package/src/router/find-match.ts +37 -1
  216. package/src/router/handler-context.ts +85 -40
  217. package/src/router/intercept-resolution.ts +161 -307
  218. package/src/router/lazy-includes.ts +43 -27
  219. package/src/router/loader-resolution.ts +21 -1
  220. package/src/router/manifest.ts +15 -10
  221. package/src/router/match-api.ts +11 -1
  222. package/src/router/match-middleware/background-revalidation.ts +2 -4
  223. package/src/router/match-middleware/cache-lookup.ts +16 -20
  224. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  225. package/src/router/match-result.ts +31 -2
  226. package/src/router/middleware.ts +6 -147
  227. package/src/router/navigation-snapshot.ts +38 -2
  228. package/src/router/prefetch-cache-ttl.ts +10 -18
  229. package/src/router/prerender-match.ts +2 -1
  230. package/src/router/revalidation.ts +24 -3
  231. package/src/router/router-context.ts +5 -18
  232. package/src/router/router-interfaces.ts +0 -13
  233. package/src/router/router-options.ts +4 -28
  234. package/src/router/segment-resolution/fresh.ts +6 -2
  235. package/src/router/segment-resolution/helpers.ts +7 -1
  236. package/src/router/segment-resolution/loader-cache.ts +4 -3
  237. package/src/router/segment-resolution/mask-nested.ts +5 -3
  238. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  239. package/src/router/segment-resolution/revalidation.ts +75 -35
  240. package/src/router/segment-resolution.ts +0 -1
  241. package/src/router/segment-wrappers.ts +8 -13
  242. package/src/router/substitute-pattern-params.ts +3 -2
  243. package/src/router/tracing.ts +2 -2
  244. package/src/router-versions.ts +7 -7
  245. package/src/router.ts +5 -17
  246. package/src/rsc/full-payload.ts +26 -4
  247. package/src/rsc/handler.ts +86 -48
  248. package/src/rsc/helpers.ts +13 -0
  249. package/src/rsc/manifest-init.ts +56 -3
  250. package/src/rsc/prefetch-render-error.ts +38 -0
  251. package/src/rsc/progressive-enhancement.ts +161 -211
  252. package/src/rsc/rewrite-passes.ts +56 -9
  253. package/src/rsc/rsc-rendering.ts +79 -23
  254. package/src/rsc/server-action.ts +25 -2
  255. package/src/rsc/shell-capture-constants.ts +26 -3
  256. package/src/rsc/shell-capture.ts +137 -51
  257. package/src/rsc/types.ts +5 -9
  258. package/src/segment-system.tsx +125 -119
  259. package/src/server/build-version-table.ts +12 -24
  260. package/src/server/context.ts +160 -7
  261. package/src/server/handle-store.ts +80 -12
  262. package/src/server/request-context.ts +46 -6
  263. package/src/server/request-headers.ts +32 -0
  264. package/src/server/rewrite-trail.ts +55 -9
  265. package/src/ssr/ssr-root.tsx +6 -10
  266. package/src/testing/build-versions.ts +0 -3
  267. package/src/testing/dispatch.ts +5 -8
  268. package/src/testing/e2e/index.ts +1 -0
  269. package/src/testing/flight.entry.ts +1 -0
  270. package/src/testing/index.ts +1 -0
  271. package/src/testing/internal/client-urls-projection.ts +31 -11
  272. package/src/testing/internal/seed-vars.ts +4 -4
  273. package/src/testing/render-route.tsx +83 -40
  274. package/src/testing/run-loader.ts +26 -2
  275. package/src/testing/run-middleware.ts +10 -0
  276. package/src/testing/run-revalidate.ts +13 -6
  277. package/src/testing/serve-shell-request.ts +79 -9
  278. package/src/testing/shell-status.ts +40 -0
  279. package/src/testing/vitest-stubs/version.ts +1 -0
  280. package/src/testing/vitest.ts +74 -7
  281. package/src/thenable-status.ts +0 -11
  282. package/src/types/handler-context.ts +12 -1
  283. package/src/types/loader-types.ts +10 -4
  284. package/src/types/route-entry.ts +2 -1
  285. package/src/urls/include-helper.ts +38 -11
  286. package/src/urls/path-helper-types.ts +4 -2
  287. package/src/vite/discovery/client-urls-projection.ts +205 -16
  288. package/src/vite/discovery/discover-routers.ts +2 -2
  289. package/src/vite/discovery/prerender-collection.ts +2 -6
  290. package/src/vite/discovery/shell-prerender-phase.ts +1 -5
  291. package/src/vite/discovery/state.ts +26 -0
  292. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  293. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  294. package/src/vite/plugins/hoist/source.ts +8 -7
  295. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  296. package/src/vite/plugins/version-plugin.ts +7 -0
  297. package/src/vite/plugins/version.d.ts +7 -0
  298. package/src/vite/plugins/virtual-entries.ts +20 -0
  299. package/src/vite/rango.ts +4 -1
  300. package/src/vite/router-discovery.ts +39 -7
  301. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -446,8 +446,8 @@ import * as CartActions from "./actions/cart";
446
446
 
447
447
  urls(({ path, loader, revalidate }) => [
448
448
  path("/", HomePage, { name: "home" }, () => [
449
- // Loader data re-runs by default after any action. Opt out with revalidate(() => false).
450
- loader(StaticHomepageLoader, () => [revalidate(() => false)]),
449
+ // Loader data re-runs by default after any action. Opt out with revalidate(false).
450
+ loader(StaticHomepageLoader, () => [revalidate(false)]),
451
451
  ]),
452
452
 
453
453
  // Re-render the cart page handler AND re-resolve its loader only after cart
@@ -603,6 +603,12 @@ Redirects from actions render the **target** route tree's matched segments
603
603
  source page's — the target is what the user sees next. See `/hooks
604
604
  useLocationState` for reading flash state on the target page.
605
605
 
606
+ Build the target by name rather than a literal path:
607
+ `throw redirect(getRequestContext().reverse("account.settings"))`. It fills
608
+ mount params (`:accountId`, `:locale`) from the page URL the action was called
609
+ on, so the module that owns the action does not need to know its mount
610
+ (`/links`).
611
+
606
612
  ### Same-origin by default (open-redirect protection)
607
613
 
608
614
  `redirect()` is same-origin by default on every path — JS, no-JS PE, and
@@ -624,6 +630,28 @@ model). Omit it and off-host targets stay blocked. `external` only waives the
624
630
  `javascript:` or `data:` URL is still neutralized, so a forged or mistaken
625
631
  `external` target can never become a scriptable navigation.
626
632
 
633
+ ## Actions on a Rewritten Page
634
+
635
+ An action cannot `throw rewrite()`; use `redirect()`. But a page that
636
+ middleware reached through `throw rewrite()` can call actions. The action
637
+ (JS or a no-JS form post) is posted to the visitor's URL, so the same
638
+ middleware sees it:
639
+
640
+ - A rewrite in **route middleware** runs after the action, around the
641
+ re-render, which route middleware sees as the page's GET, so a rewrite
642
+ written for GETs only fires too. The action runs once, then the re-render
643
+ runs as the target: its global and route middleware, its handlers and
644
+ loaders. The page on screen is revalidated in place, as at the target's
645
+ own URL.
646
+ - A rewrite in **global middleware** runs before the action and moves the
647
+ mutation to the target first: the target's global middleware, then the
648
+ action, once, then the target's route middleware and render, as a direct
649
+ post to the target. A page a routing rewrite showed (a multi-tenant host,
650
+ an A/B split) keeps its forms working, in JS and no-JS. A lock refuses
651
+ mutations instead: `throw rewrite("maintenance").disallowMutations()` is an
652
+ error before the action runs, or return a `Response` for mutations. See
653
+ `/middleware` ("Mutations: where the middleware sits decides").
654
+
627
655
  ## Error Handling
628
656
 
629
657
  ### Validation errors — return them as state
@@ -688,24 +716,36 @@ For a recoverable "already gone" case, return a result the form can show
688
716
  ### A loader that fails on the revalidation render
689
717
 
690
718
  A loader that throws while it reruns after the action does not undo the
691
- action, which has already run and returned. What follows depends on the route:
692
-
693
- - **No `loading()` on the route (and any `clientUrls()` route):** the action
694
- call rejects with the loader's error, nothing of that revalidation commits,
695
- and the page keeps its data and its React state. A `<form action>`,
696
- `useActionState` or `useTransition` call hands that rejection to the nearest
697
- React error boundary above the form.
698
- - **A `urls()` route with `loading()`:** the call resolves with the action's
699
- result, the page renders, and the loader's read throws to the nearest React
700
- error boundary.
701
- - **An `errorBoundary()` above the route:** its fallback replaces the route.
702
-
703
- For a failure the user can retry, return it from the loader as data
704
- (`{ ok: false, reason }`) and render it where the data goes. A failure returned
705
- as data is a value like any other, so a `cache()` on the loader (or a `ppr`
706
- bake, or a prerender) stores and serves it; a thrown error is never stored.
707
- See `/client-urls` → "A loader that fails on its rerun" for the table and an
708
- example.
719
+ action, which has already run and returned. It behaves as on a navigation (a
720
+ plain or fully prefetched click, Back/Forward), on every route, called
721
+ directly or from `<form action>`, `useActionState` or `useTransition`:
722
+
723
+ - The call resolves with the action's result.
724
+ - The loader throws in place, at its `useLoader` read, and the nearest
725
+ boundary to the read catches it: your own React error boundary around the
726
+ read, else the route's `errorBoundary()`, else the router's root one.
727
+ Everything outside that boundary keeps its React state; loaders that reran
728
+ without error commit their new data.
729
+ - The error goes to `onError` on the server (phase `"loader"`) and to
730
+ `useAction(action).revalidationError` in the browser (`error` stays `null`,
731
+ `result` is set).
732
+ - A later rerun that succeeds clears an `errorBoundary()` fallback. Your own
733
+ boundary resets by its own rule: key it on data that reruns with the loader
734
+ and is read outside the boundary.
735
+
736
+ `notFound()` and `redirect()` from the loader keep their path: a `redirect()`
737
+ replaces the route's content and navigates, and the layouts keep their
738
+ instance. Every refetch the router runs for an action follows the same rule
739
+ (after several actions at once, after a navigation during it), and the action
740
+ that ran it reports the loader's error as its `revalidationError`. Not
741
+ covered yet: an `intercept()`'s loaders, and a parallel slot's loaders under
742
+ its own `loading()`. For a failure the user should see in place of the
743
+ data without an error boundary, return it from the loader as data
744
+ (`{ ok: false, reason }`) and render it where the data goes. A failure
745
+ returned as data is a value like any other, so a `cache()` on the loader (or
746
+ a `ppr` bake, or a prerender) stores and serves it; a thrown error is never
747
+ stored. See `/client-urls` → "A loader that fails on its rerun" for the
748
+ details and examples.
709
749
 
710
750
  ### Authorization in actions
711
751
 
@@ -155,6 +155,7 @@ beside their function):
155
155
  | | `setBuildVersions` | [`serve-shell-request.md`](./serve-shell-request.md) |
156
156
  | | `assertCacheStatus`, `assertCacheDecision`, `parseCacheHeader`, `createCacheSink`, `filterCacheDecisions` | [`cache-prerender.md`](./cache-prerender.md) |
157
157
  | | `assertShellStatus`, `parseShellStatus`, `shellCacheKey`, `SHELL_STATUS_HEADER`, `assertPprReplayStatus`, `parsePprReplayStatus`, `PPR_REPLAY_STATUS_HEADER` | [`cache-prerender.md`](./cache-prerender.md) |
158
+ | | `visitorShellCacheKey` | [`serve-shell-request.md`](./serve-shell-request.md) |
158
159
  | | `assertGeneratedRoutesMatch`, `diffGeneratedRoutes` | [`reverse-and-types.md`](./reverse-and-types.md) |
159
160
  | `@rangojs/router/testing/dom` | `renderRoute` | [`client-components.md`](./client-components.md) |
160
161
  | `@rangojs/router/testing/flight` | `renderToFlightString`, `normalizeFlight`, `assertFlightRuntimeAvailable` | [`flight.md`](./flight.md) |
@@ -38,7 +38,7 @@
38
38
  | `useReverse` | Reverse a name->pattern map to a URL; merges `useParams()` and the `mount`/`basename` prefix. |
39
39
  | `useHref` | Resolve an href against the mount/basename. |
40
40
  | `useMount` | The `include()` mount prefix (`options.mount`), else `"/"`. |
41
- | `useNavigation` | Navigation controller state — stays `idle` (see caveat). |
41
+ | `useNavigation` | Navigation controller state: `loading` from a `navigate()` call to its commit, else `idle`. |
42
42
  | `useRouter` | The router handle, including `.basename`. |
43
43
  | `usePathname` | Current committed pathname. |
44
44
  | `useSearchParams` | Search params from the `request` URL. |
@@ -376,14 +376,17 @@ it("dims the held price while the next product streams", async () => {
376
376
  });
377
377
  ```
378
378
 
379
- | Pending state | Under `renderRoute` |
380
- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
381
- | `useLoader().isLoading` on the reader a `transition` navigate() holds, pending seed | Modeled: `true` on the OLD data until the promise settles, then `false` with the new data in the same commit. |
382
- | `transition` navigate(), settled seed (a plain value) | `false`; the data swaps in the commit. |
383
- | navigate() without `transition`, pending seed | Urgent commit (production's cold-navigation lane): the read suspends to the nearest `<Suspense>` fallback (with none, React keeps the previous tree). No flag. |
384
- | `transition: { when }` returning false, or `navigate(url, { transition: false })` | Same as without `transition`: urgent, with production's decision (`kind: "push"`; `refresh()` is `"revalidate"`). `transition: false` calls no predicate. |
385
- | Production's other hold lanes (same-structure search/filter nav, fully-prefetched, optimistic) | NOT modeled — without `transition` navigate() stays urgent. Assert those at e2e. |
386
- | `useNavigation().state`, `useLinkStatus().pending`, `useAction().state` | Stay `idle` — navigate() never starts the navigation lifecycle. e2e. |
379
+ | Pending state | Under `renderRoute` |
380
+ | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
381
+ | `useLoader().isLoading` on the reader a `transition` navigate() holds, pending seed | Modeled: `true` on the OLD data until the promise settles, then `false` with the new data in the same commit. |
382
+ | `transition` navigate(), settled seed (a plain value) | `false`; the data swaps in the commit. |
383
+ | navigate() without `transition`, pending seed | Urgent commit (production's cold-navigation lane): the read suspends to the nearest `<Suspense>` fallback (with none, React keeps the previous tree). No flag. |
384
+ | `transition: { when }` returning false, or `navigate(url, { transition: false })` | Same as without `transition`: urgent, with production's decision (`kind: "push"`; `refresh()` is `"revalidate"`). `transition: false` calls no predicate. |
385
+ | Production's other hold lanes (same-structure search/filter nav, fully-prefetched, optimistic) | NOT modeled — without `transition` navigate() stays urgent. Assert those at e2e. |
386
+ | `useNavigation().state`, `useLinkStatus().pending` | Modeled: navigate() starts the navigation as a click does. `loading` from the call, `idle` in the commit, held with it; `pending` true for the destination's link until the commit is handed to React. |
387
+ | `useAction().state` | Stays `idle`: navigate() runs no action. e2e. |
388
+ | `useHandle()` on a held navigation, `navigate(url, { handles, loaders })` | Modeled: `handles` arrive on the response's handle stream, so readers keep the page being left's values while React holds the page and take the new ones with its commit. |
389
+ | A back/forward restored from the history cache (its view transition, `pop`) | NOT modeled: the tree has one location and navigate() caches no entries, so a `popstate` restores nothing. Assert it at e2e. |
387
390
 
388
391
  - Per-navigation seeds merge over the render-time `loaders`/`loaderData` for THAT navigation only; a later `navigate()` without `loaders` falls back to the render-time data.
389
392
  - Pass settled data as a plain value, not `Promise.resolve(value)`: React has not observed a plain promise yet, so it counts as pending and renders one `stale` frame. (A Flight chunk that has already settled carries its status and is skipped, which a plain value mirrors.)
@@ -392,7 +395,7 @@ it("dims the held price while the next product streams", async () => {
392
395
  ## Caveats
393
396
 
394
397
  - Client tree ONLY. Does NOT catch server/client boundary reference-identity remount bugs, real Flight serialization errors, loader execution, middleware, or handler ordering — those are `renderServerTree` / `renderHandler` / e2e territory. Loader data is SEEDED, never run. `hydrate: true` adds an HTML pass of this same client tree ([Hydration](#hydration)), not a render of your routes on a server.
395
- - `router.navigate()` bypasses the navigation lifecycle, so the controller never leaves `idle`. `useNavigation()` / `useLinkStatus()` / `useAction()` non-idle states (loading/streaming/pending, action result/error) are NOT reachable — test those at e2e. The one modeled pending state is held-navigation `useLoader().isLoading` ([Held navigation](#held-navigation)).
398
+ - `router.navigate()` drives the navigation lifecycle: `useNavigation()` reads `loading` from the call and `idle` in the commit, held with it for a `transition` spec ([Held navigation](#held-navigation)). `useAction()` non-idle states (action loading/streaming, result/error) are NOT reachable — no action runs; test those at e2e.
396
399
  - `outletPending` seeds only the production-shaped outlet context. It is useful
397
400
  for the two settled render states of a layout that reads `useOutlet()`, but it
398
401
  does not prove the hydrated `clientUrls()` transition that toggles the value;
@@ -8,29 +8,30 @@
8
8
 
9
9
  ### Options — `RunLoaderOptions<TEnv>`
10
10
 
11
- | Field | Type | Meaning |
12
- | --------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
- | `params` | `Record<string, string>` | Route params; surfaced as `ctx.params` and `ctx.routeParams`. |
14
- | `search` | `Record<string, string>` | Search params; merged into the request URL so `ctx.searchParams` reflects them. |
15
- | `searchData` | `Record<string, unknown>` | The TYPED `ctx.search` object a route's search schema would produce. Distinct from `search` (which sets the raw `ctx.searchParams`). |
16
- | `basename` | `string` | Router basename surfaced on the context; drives `redirect()` prefixing. |
17
- | `theme` | `ThemeConfig \| true` | Theme config in the `createRouter({ theme })` shape (e.g. `true` or `{ themes: [...] }`). Without it `ctx.theme`/`ctx.setTheme` are inert. |
18
- | `env` | `TEnv` | Environment bindings surfaced as `ctx.env`. |
19
- | `request` | `Request \| string` | Override the backing Request. Defaults to a localhost GET. |
20
- | `vars` | `VarsInit` | Variables a prior middleware would have set (object `{ key: value }`, or `[key, value]` tuples where the key may be a `createVar()` handle). |
21
- | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()`. |
22
- | `routeName` | `string` | Matched route name for scoped `.name` reverse resolution. |
23
- | `method` | `string` | HTTP method surfaced as `ctx.method`. Defaults to `"GET"`. |
24
- | `body` | `unknown` | Request body surfaced as `ctx.body`. |
25
- | `formData` | `FormData` | Form data surfaced as `ctx.formData` (exposed verbatim; no multipart parsing). |
26
- | `loaders` | `ReadonlyArray<readonly [LoaderDefinition<any, any>, unknown]>` | Seed `ctx.use(OtherLoader)` by REFERENCE as `[[OtherLoader, data]]` tuples (same shape as `renderHandler`/`renderRoute`). Checked before `use`. |
27
- | `use` | `UseResolver` | Dynamic resolver for `ctx.use(OtherLoader)` composition. `loaders` wins when both match. |
28
- | `cacheStore` | `SegmentCacheStore` | Cache store backing `use cache` functions. Without one, a cached function bypasses and runs uncached (its taint/profile guards never fire). |
29
- | `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles, the `createRouter({ cacheProfiles })` shape. |
30
- | `cache` | `PartialCacheOptions` | Run the loader through the `cache()` a route binds it with (`loader(Loader, () => [cache({...})])` takes the same options), against `cache.store`, else `cacheStore`. See the caveat below. |
31
- | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a loader calling `invalidateClientCache()` rotates (the name is always seeded — default `rango-state_router_0`). |
32
- | `rendered` | `boolean \| (() => void \| Promise<void>)` | Mock the `ctx.rendered()` render barrier so a loader that `await ctx.rendered()`s can be unit-tested. By default `ctx.rendered()` throws. `true` resolves immediately; a function controls timing/side effects. |
33
- | `handles` | `ReadonlyArray<readonly [Handle<any, any>, unknown]>` | Seed the values `ctx.get(SomeHandle)` returns — the ACCUMULATED handle data read after `await ctx.rendered()`. Matched by handle reference. (Loader handle WRITES need no seed — see `runLoaderResult(...).handlePushes`.) |
11
+ | Field | Type | Meaning |
12
+ | --------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `params` | `Record<string, string>` | Route params; surfaced as `ctx.params` and `ctx.routeParams`. |
14
+ | `search` | `Record<string, string>` | Search params; merged into the request URL so `ctx.searchParams` reflects them. |
15
+ | `searchData` | `Record<string, unknown>` | The TYPED `ctx.search` object a route's search schema would produce. Distinct from `search` (which sets the raw `ctx.searchParams`). |
16
+ | `basename` | `string` | Router basename surfaced on the context; drives `redirect()` prefixing. |
17
+ | `theme` | `ThemeConfig \| true` | Theme config in the `createRouter({ theme })` shape (e.g. `true` or `{ themes: [...] }`). Without it `ctx.theme`/`ctx.setTheme` are inert. |
18
+ | `env` | `TEnv` | Environment bindings surfaced as `ctx.env`. |
19
+ | `request` | `Request \| string` | Override the backing Request. Defaults to a localhost GET. |
20
+ | `vars` | `VarsInit` | Variables a prior middleware would have set (object `{ key: value }`, or `[key, value]` tuples where the key may be a `createVar()` handle). |
21
+ | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()`. |
22
+ | `routeName` | `string` | Matched route name for scoped `.name` reverse resolution. |
23
+ | `declaredIn` | `string` | Include name prefix of the module whose `layout()` / `parallel()` / `cache()` declares the loader (`"account"`, `""` for the root module). `.name` then resolves in that module, as such a loader does in production, instead of from `routeName`. |
24
+ | `method` | `string` | HTTP method surfaced as `ctx.method`. Defaults to `"GET"`. |
25
+ | `body` | `unknown` | Request body surfaced as `ctx.body`. |
26
+ | `formData` | `FormData` | Form data surfaced as `ctx.formData` (exposed verbatim; no multipart parsing). |
27
+ | `loaders` | `ReadonlyArray<readonly [LoaderDefinition<any, any>, unknown]>` | Seed `ctx.use(OtherLoader)` by REFERENCE as `[[OtherLoader, data]]` tuples (same shape as `renderHandler`/`renderRoute`). Checked before `use`. |
28
+ | `use` | `UseResolver` | Dynamic resolver for `ctx.use(OtherLoader)` composition. `loaders` wins when both match. |
29
+ | `cacheStore` | `SegmentCacheStore` | Cache store backing `use cache` functions. Without one, a cached function bypasses and runs uncached (its taint/profile guards never fire). |
30
+ | `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles, the `createRouter({ cacheProfiles })` shape. |
31
+ | `cache` | `PartialCacheOptions` | Run the loader through the `cache()` a route binds it with (`loader(Loader, () => [cache({...})])` takes the same options), against `cache.store`, else `cacheStore`. See the caveat below. |
32
+ | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a loader calling `invalidateClientCache()` rotates (the name is always seeded — default `rango-state_router_0`). |
33
+ | `rendered` | `boolean \| (() => void \| Promise<void>)` | Mock the `ctx.rendered()` render barrier so a loader that `await ctx.rendered()`s can be unit-tested. By default `ctx.rendered()` throws. `true` resolves immediately; a function controls timing/side effects. |
34
+ | `handles` | `ReadonlyArray<readonly [Handle<any, any>, unknown]>` | Seed the values `ctx.get(SomeHandle)` returns — the ACCUMULATED handle data read after `await ctx.rendered()`. Matched by handle reference. (Loader handle WRITES need no seed — see `runLoaderResult(...).handlePushes`.) |
34
35
 
35
36
  ### Context — `TestLoaderContext<TEnv>` (what your loader receives)
36
37
 
@@ -116,7 +117,7 @@ it("asserts a loader's set-cookie + redirect (runLoaderResult)", async () => {
116
117
 
117
118
  ## Caveats
118
119
 
119
- - `ctx.reverse(...)` throws unless you pass `routeMap` (and `routeName` for scoped `.name` resolution). It does NOT fall back to the global route map.
120
+ - `ctx.reverse(...)` throws unless you pass `routeMap` (and `routeName` for scoped `.name` resolution, or `declaredIn` for a layout's or parallel's loader). It does NOT fall back to the global route map.
120
121
  - `ctx.rendered()` throws by default (the render barrier only exists in a full match); pass `{ rendered: true }` to mock it for post-barrier logic, and `{ handles: [[SomeHandle, data]] }` to seed the `ctx.get(SomeHandle)` read.
121
122
  - A loader has no `isAction()`: whether it re-runs after an action is decided by `revalidate()` on its registration, not inside the loader body. Assert post-action re-runs at e2e.
122
123
  - Seeded `loaders` (by-reference tuples) are NOT executed — `ctx.use(OtherLoader)` returns the seeded value. The dynamic `use` resolver, by contrast, IS executed (it is a function called to compute the value). Either way the REAL loader body is not run; real loader execution and side-effects are e2e-only. `loaders` is checked before the `use` resolver.
@@ -44,17 +44,17 @@ The `ctx` your middleware reads. Notable fields:
44
44
 
45
45
  ### Returns — `RunMiddlewareResult<TEnv>`
46
46
 
47
- | Field | Type | Meaning |
48
- | ----------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
49
- | `response` | `Response` | The final Response: the downstream response, or a middleware short-circuit. |
50
- | `ctx` | `RequestContext<TEnv>` | The underlying RequestContext (NOT a per-middleware `MiddlewareContext`). Use `ctx.get(...)` for anything the envelope above doesn't surface. |
51
- | `nextCalled` | `number` | Times the terminal handler ran: `0` on short-circuit, `1` on pass-through. |
52
- | `rewrite` | `RunMiddlewareRewrite?` | The target of a `throw rewrite(...)`: `{ pathname, search, routeName? }`, resolved as production resolves it (a path basename-prefixed, a name through `routeMap`). `undefined` when nothing rewrote. On a rewrite, `response` carries what the chain set before the throw. |
53
- | `dynamic` | `boolean` | Whether the chain called `ctx.dynamic()` (the PPR shell opt-out). |
54
- | `cookies` | `Record<string, string>` | Effective cookie view: request cookies merged with chain sets/deletes (last-write-wins), as `{ name: value }`. |
55
- | `headers` | `Record<string, string>` | Final response headers as `{ name: value }`, lowercased, EXCLUDING `set-cookie` (use `cookies`). |
56
- | `locationState` | `Record<string, unknown>` | Flat `{ key: value }` state set via `setLocationState()` / `redirect({ state })` (empty when none). |
57
- | `stateCookieName` | `string` | The resolved rango state cookie name seeded for the run (default `rango-state_router_0`). Assert a middleware's `invalidateClientCache()` rotation against it without recomputing — parity with `runInRequestContext` / `runLoaderResult` / `renderHandler`. |
47
+ | Field | Type | Meaning |
48
+ | ----------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
49
+ | `response` | `Response` | The final Response: the downstream response, or a middleware short-circuit. |
50
+ | `ctx` | `RequestContext<TEnv>` | The underlying RequestContext (NOT a per-middleware `MiddlewareContext`). Use `ctx.get(...)` for anything the envelope above doesn't surface. |
51
+ | `nextCalled` | `number` | Times the terminal handler ran: `0` on short-circuit, `1` on pass-through. |
52
+ | `rewrite` | `RunMiddlewareRewrite?` | The target of a `throw rewrite(...)`: `{ pathname, search, routeName?, mutationsDisallowed? }` (`mutationsDisallowed: true` only for `.disallowMutations()`, absent otherwise), resolved as production resolves it (a path basename-prefixed, a name through `routeMap`). `undefined` when nothing rewrote. On a rewrite, `response` carries what the chain set before the throw. |
53
+ | `dynamic` | `boolean` | Whether the chain called `ctx.dynamic()` (the PPR shell opt-out). |
54
+ | `cookies` | `Record<string, string>` | Effective cookie view: request cookies merged with chain sets/deletes (last-write-wins), as `{ name: value }`. |
55
+ | `headers` | `Record<string, string>` | Final response headers as `{ name: value }`, lowercased, EXCLUDING `set-cookie` (use `cookies`). |
56
+ | `locationState` | `Record<string, unknown>` | Flat `{ key: value }` state set via `setLocationState()` / `redirect({ state })` (empty when none). |
57
+ | `stateCookieName` | `string` | The resolved rango state cookie name seeded for the run (default `rango-state_router_0`). Assert a middleware's `invalidateClientCache()` rotation against it without recomputing — parity with `runInRequestContext` / `runLoaderResult` / `renderHandler`. |
58
58
 
59
59
  ## Recipe
60
60
 
@@ -8,23 +8,23 @@ A Rango route handler is a pure function `(ctx) => rsc` — the function you pas
8
8
 
9
9
  ### Options — `RenderHandlerOptions`
10
10
 
11
- | Field | Type | Meaning |
12
- | ---------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
- | `params` | `Record<string, string>` | Route params surfaced as `ctx.params`. |
14
- | `env` | `TEnv` | Environment bindings surfaced as `ctx.env`. |
15
- | `request` | `Request \| string` | Backing Request (string or `Request`); defaults to a localhost GET. |
16
- | `headers` | `HeadersInit` | Request headers (e.g. `Cookie`) the handler reads via `cookies()`. Used only when `request` is a string or omitted; a passed `Request` keeps its own headers. |
17
- | `build` | `boolean` | Seed `ctx.build` (default `false`) to test a handler that branches on the build-time pass, including calling `ctx.dynamic()` on a miss. Assert the opt-out via `result.dynamic`. |
18
- | `theme` | `ThemeConfig \| true` | Theme config (`createRouter({ theme })` shape). Without it `ctx.theme`/`ctx.setTheme` are inert; pass one to test a handler that reads the theme or writes the theme cookie. |
19
- | `vars` | `VarsInit` (object or `[[Var, value]]` tuples) | Variables a prior middleware set, read via `ctx.get(...)`. |
20
- | `routeName` | `string` | Matched route name (drives `ctx.routeName` and scoped reverse). |
21
- | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()`. |
22
- | `loaders` | `ReadonlyArray<readonly [LoaderDefinition, unknown]>` | Seed the data `ctx.use(SomeLoader)` returns. Matched by loader reference; NO real loader runs. |
23
- | `clientComponents` | `Record<string, unknown>` | `"use client"` components in the handler's RSC, so they serialize as real boundaries when `rangoUseClientTransform()` is not wired. Keyed by name. |
24
- | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a handler calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production rather than no-opping; override `prefix`/`routerId` to match your `createRouter({ stateCookiePrefix, id })`, or `version` (the app's version, as `createRouter({ version })` would set it; the value is `{version}:{timestamp}`, default `"0"`). |
25
- | `cacheStore` | `SegmentCacheStore` | Segment cache store backing a `"use cache"` function the handler invokes (e.g. `new MemorySegmentCacheStore()`). WITHOUT it, `registerCachedFunction` takes the uncached bypass and the cached path is NOT exercised (the runtime emits a one-time warning under the test runner). Pair with `cacheProfiles`. |
26
- | `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles in the `createRouter({ cacheProfiles })` shape, required for `"use cache: profileName"` resolution once a `cacheStore` is wired. |
27
- | `inActionRevalidation` | `boolean` | Render as if inside a server action's revalidation render (production sets this in `revalidateAfterAction`). A stale `"use cache"` entry whose profile sets `foregroundOnAction: true` then re-executes in the FOREGROUND (fresh result in this render) instead of SWR. Pair with `cacheStore` + `cacheProfiles` to exercise the opt-in. |
11
+ | Field | Type | Meaning |
12
+ | ---------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `params` | `Record<string, string>` | Route params surfaced as `ctx.params`. |
14
+ | `env` | `TEnv` | Environment bindings surfaced as `ctx.env`. |
15
+ | `request` | `Request \| string` | Backing Request (string or `Request`); defaults to a localhost GET. |
16
+ | `headers` | `HeadersInit` | Request headers (e.g. `Cookie`) the handler reads via `cookies()`. Used only when `request` is a string or omitted; a passed `Request` keeps its own headers. |
17
+ | `build` | `boolean` | Seed `ctx.build` (default `false`) to test a handler that branches on the build-time pass, including calling `ctx.dynamic()` on a miss. Assert the opt-out via `result.dynamic`. |
18
+ | `theme` | `ThemeConfig \| true` | Theme config (`createRouter({ theme })` shape). Without it `ctx.theme`/`ctx.setTheme` are inert; pass one to test a handler that reads the theme or writes the theme cookie. |
19
+ | `vars` | `VarsInit` (object or `[[Var, value]]` tuples) | Variables a prior middleware set, read via `ctx.get(...)`. |
20
+ | `routeName` | `string` | Matched route name (drives `ctx.routeName` and scoped reverse). |
21
+ | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()`. |
22
+ | `loaders` | `ReadonlyArray<readonly [LoaderDefinition, unknown]>` | Seed the data `ctx.use(SomeLoader)` returns. Matched by loader reference; NO real loader runs. |
23
+ | `clientComponents` | `Record<string, unknown>` | `"use client"` components in the handler's RSC, so they serialize as real boundaries when `rangoUseClientTransform()` is not wired. Keyed by name. |
24
+ | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a handler calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production rather than no-opping; override `prefix`/`routerId` to match your `createRouter({ stateCookiePrefix, id })`, or `version` (a seeded app version: the value is `{version}:{timestamp}`, default `"0"`). |
25
+ | `cacheStore` | `SegmentCacheStore` | Segment cache store backing a `"use cache"` function the handler invokes (e.g. `new MemorySegmentCacheStore()`). WITHOUT it, `registerCachedFunction` takes the uncached bypass and the cached path is NOT exercised (the runtime emits a one-time warning under the test runner). Pair with `cacheProfiles`. |
26
+ | `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles in the `createRouter({ cacheProfiles })` shape, required for `"use cache: profileName"` resolution once a `cacheStore` is wired. |
27
+ | `inActionRevalidation` | `boolean` | Render as if inside a server action's revalidation render (production sets this in `revalidateAfterAction`). A stale `"use cache"` entry whose profile sets `foregroundOnAction: true` then re-executes in the FOREGROUND (fresh result in this render) instead of SWR. Pair with `cacheStore` + `cacheProfiles` to exercise the opt-in. |
28
28
 
29
29
  ### Context — `HandlerContext` (what your handler receives)
30
30
 
@@ -4,6 +4,8 @@
4
4
 
5
5
  `dispatch` runs the router's REAL matching (reusing `previewMatch`) and the real global + route-level middleware chain, with no RSC render — so redirects, 404s, response routes, content negotiation, and middleware short-circuits behave exactly as in production. You SEED the request and `env`; everything else (matching, middleware, header/cookie merge) is real machinery.
6
6
 
7
+ Matching goes through the route trie, as every request in `vite dev` and production does.
8
+
7
9
  ## API
8
10
 
9
11
  ### Options — `DispatchOptions<TEnv>`
@@ -78,7 +80,7 @@ describe("api routes via dispatch", () => {
78
80
 
79
81
  `dispatch` also covers, with production code paths:
80
82
 
81
- - trailing-slash/redirect targets (`findMatch`) — a redirected path returns a 308 with the `Location` (query preserved);
83
+ - trailing-slash redirects from the trie — a route with `trailingSlash: "never"` or `"always"` answers the other form with a 308 and the `Location` (query preserved). A route with no `trailingSlash` serves its URL with and without a trailing slash, with no redirect, as production does;
82
84
  - the open-redirect guard on browser-followed redirects (a cross-origin `Location` is rewritten to the basename root unless `redirect(url, { external: true })`);
83
85
  - content-negotiated routes (`Vary: Accept` appended);
84
86
  - cached response routes (`cache({ ... })` around a `path.json` etc.): hit, SWR, and tag writes. The cache write is scheduled via `waitUntil` (a microtask in tests), so flush microtasks between the seeding `dispatch` and the one that asserts the hit;
@@ -90,6 +92,7 @@ describe("api routes via dispatch", () => {
90
92
  - A COMPONENT route's guard stack cannot run here. Assert it at e2e, or extract the middleware fn and unit-test it with `runMiddleware` (see `./middleware.md`).
91
93
  - JSON serialization is bare, applied in `response-route-handler.ts`: a `path.json` handler that returns a value is serialized verbatim (`JSON.stringify(value)`, status 200, `application/json`) — no envelope. Returning or throwing a `Response` (e.g. `Response.json(x)`) uses the same control-flow path. Any other thrown error takes the error's status (`RouterError.status`, else 500, or a non-200 status already set upstream in the request pipeline). On a `path.json` route the body is an RFC 9457 problem+json object `{ title, status, detail, code }` (`application/problem+json;charset=utf-8`), where `code` is the `RouterError.code`, else `"INTERNAL"`, and the `type` member is omitted. On text/html/xml/md routes the body is `text/plain`: the `RouterError` message, the error message in dev, or `"Internal Server Error"` in production. Assert the shape matching what your handler returns.
92
94
  - Setup: needs the preset (alias + virtual stubs) or a Vite-RSC env (see `./setup.md`); a bare router import throws on Vite virtuals.
95
+ - Building the trie runs route discovery: every `include()`d `urls()` handler runs once more, and every async include provider (`() => import(...)`) is called. Count provider calls or handler runs after the first `dispatch` on that router, not from router construction. A provider that rejects fails the `dispatch`, as it fails `vite dev` and the build.
93
96
  - A router using `Prerender()`/`createLoader()`/`Static()` now constructs in a bare test (each assigns a runtime fallback `$$id`). Importing the whole router _file_ may still need the plugin (its page modules pull app deps / `virtual:` modules) — build from a focused include (your API routes) for whole-router dispatch.
94
97
  - A `throw rewrite(target)` from global or route middleware re-dispatches as the router does: global middleware matched on the target's path, the target's route middleware and handler, the same request context, `x-rango-rewrite` on the response, 508 on a loop. The visitor's URL is matched before middleware runs, so it must be a response route (or a redirect or 404) too; a rewrite target that is a component route throws the same error as a direct request.
95
98
  - A `_rsc_partial` request to a response route runs global middleware first (an auth gate can still 401/redirect), then returns `X-RSC-Reload` — route-level middleware is skipped, exactly like production.