@rangojs/router 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/README.md +255 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/cookie-name.d.ts +6 -0
  4. package/dist/types/browser/history-state.d.ts +16 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  6. package/dist/types/browser/partial-update.d.ts +11 -0
  7. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  8. package/dist/types/browser/rsc-router.d.ts +1 -1
  9. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  10. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  11. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  12. package/dist/types/cache/cache-policy.d.ts +17 -0
  13. package/dist/types/cache/cache-scope.d.ts +132 -19
  14. package/dist/types/cache/cache-tag.d.ts +92 -7
  15. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  18. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  19. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  20. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  21. package/dist/types/cache/handle-capture.d.ts +41 -3
  22. package/dist/types/cache/handle-snapshot.d.ts +54 -13
  23. package/dist/types/cache/index.d.ts +1 -0
  24. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  25. package/dist/types/cache/segment-codec.d.ts +10 -3
  26. package/dist/types/cache/shell-memo.d.ts +178 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  28. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  29. package/dist/types/cache/taint.d.ts +1 -1
  30. package/dist/types/cache/types.d.ts +220 -44
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  32. package/dist/types/client.d.ts +4 -4
  33. package/dist/types/client.rsc.d.ts +4 -5
  34. package/dist/types/components/DefaultDocument.d.ts +5 -2
  35. package/dist/types/context-var.d.ts +6 -5
  36. package/dist/types/escape-script.d.ts +1 -1
  37. package/dist/types/handles/MetaTags.d.ts +5 -4
  38. package/dist/types/handles/Scripts.d.ts +8 -7
  39. package/dist/types/handles/meta.d.ts +2 -2
  40. package/dist/types/handles/script.d.ts +17 -16
  41. package/dist/types/href-client.d.ts +1 -1
  42. package/dist/types/html.d.ts +12 -0
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +3 -2
  45. package/dist/types/loader-store.d.ts +7 -4
  46. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  47. package/dist/types/reverse.d.ts +36 -4
  48. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  49. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  50. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  51. package/dist/types/route-types.d.ts +1 -1
  52. package/dist/types/router/debug-manifest.d.ts +3 -2
  53. package/dist/types/router/error-handling.d.ts +20 -0
  54. package/dist/types/router/handler-context.d.ts +6 -0
  55. package/dist/types/router/instrument.d.ts +8 -0
  56. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  57. package/dist/types/router/middleware-types.d.ts +9 -3
  58. package/dist/types/router/router-context.d.ts +3 -1
  59. package/dist/types/router/router-interfaces.d.ts +24 -2
  60. package/dist/types/router/router-options.d.ts +8 -5
  61. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  62. package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
  63. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  64. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
  65. package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  67. package/dist/types/router/segment-wrappers.d.ts +1 -1
  68. package/dist/types/rsc/full-payload.d.ts +14 -0
  69. package/dist/types/rsc/handler.d.ts +1 -1
  70. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  71. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  72. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
  74. package/dist/types/rsc/shell-capture.d.ts +151 -19
  75. package/dist/types/rsc/shell-serve.d.ts +140 -17
  76. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  77. package/dist/types/rsc/types.d.ts +17 -0
  78. package/dist/types/server/context.d.ts +139 -15
  79. package/dist/types/server/cookie-store.d.ts +21 -0
  80. package/dist/types/server/handle-store.d.ts +26 -2
  81. package/dist/types/server/request-context.d.ts +182 -53
  82. package/dist/types/ssr/index.d.ts +24 -0
  83. package/dist/types/ssr/ssr-root.d.ts +9 -8
  84. package/dist/types/static-handler.d.ts +4 -1
  85. package/dist/types/testing/flight.entry.d.ts +7 -0
  86. package/dist/types/testing/generated-routes.d.ts +2 -2
  87. package/dist/types/testing/internal/context.d.ts +1 -1
  88. package/dist/types/testing/render-route.d.ts +32 -6
  89. package/dist/types/testing/run-loader.d.ts +18 -3
  90. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  91. package/dist/types/testing/shell-status.d.ts +23 -10
  92. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  93. package/dist/types/testing/vitest.d.ts +35 -1
  94. package/dist/types/theme/ThemeScript.d.ts +3 -3
  95. package/dist/types/types/cache-types.d.ts +14 -10
  96. package/dist/types/types/handler-context.d.ts +28 -1
  97. package/dist/types/types/loader-types.d.ts +8 -6
  98. package/dist/types/urls/pattern-types.d.ts +22 -18
  99. package/dist/types/urls/response-types.d.ts +3 -12
  100. package/dist/types/use-loader.d.ts +12 -3
  101. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  102. package/dist/types/vite/rango.d.ts +1 -1
  103. package/dist/types/vite/router-discovery.d.ts +29 -2
  104. package/dist/vite/index.js +1420 -1348
  105. package/package.json +2 -2
  106. package/skills/api-client/SKILL.md +22 -10
  107. package/skills/breadcrumbs/SKILL.md +75 -22
  108. package/skills/bundle-analysis/SKILL.md +22 -22
  109. package/skills/cache-guide/SKILL.md +306 -124
  110. package/skills/caching/SKILL.md +530 -98
  111. package/skills/catalog.json +16 -16
  112. package/skills/client-urls/SKILL.md +65 -38
  113. package/skills/cloudflare/SKILL.md +87 -20
  114. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  115. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  116. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  117. package/skills/comparison/SKILL.md +5 -0
  118. package/skills/comparison/references/framework-comparison.md +75 -57
  119. package/skills/composability/SKILL.md +38 -29
  120. package/skills/css/SKILL.md +10 -13
  121. package/skills/debug-manifest/SKILL.md +74 -31
  122. package/skills/defer-hydration/SKILL.md +52 -31
  123. package/skills/deployment-caching/SKILL.md +11 -3
  124. package/skills/document-cache/SKILL.md +98 -28
  125. package/skills/fonts/SKILL.md +16 -8
  126. package/skills/handler-use/SKILL.md +55 -17
  127. package/skills/hooks/SKILL.md +28 -20
  128. package/skills/hooks/data.md +23 -3
  129. package/skills/hooks/handle-and-actions.md +34 -17
  130. package/skills/hooks/navigation.md +39 -10
  131. package/skills/hooks/outlets.md +13 -6
  132. package/skills/hooks/state.md +39 -15
  133. package/skills/hooks/urls.md +7 -5
  134. package/skills/host-router/SKILL.md +64 -12
  135. package/skills/i18n/SKILL.md +60 -34
  136. package/skills/intercept/SKILL.md +161 -26
  137. package/skills/layout/SKILL.md +106 -50
  138. package/skills/links/SKILL.md +51 -19
  139. package/skills/loader/SKILL.md +386 -128
  140. package/skills/middleware/SKILL.md +129 -26
  141. package/skills/migrate-nextjs/SKILL.md +156 -70
  142. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  143. package/skills/migrate-react-router/SKILL.md +51 -39
  144. package/skills/migrate-react-router/component-migration.md +28 -7
  145. package/skills/migrate-react-router/data-and-actions.md +21 -6
  146. package/skills/migrate-react-router/route-mapping.md +19 -10
  147. package/skills/mime-routes/SKILL.md +12 -7
  148. package/skills/observability/SKILL.md +92 -14
  149. package/skills/parallel/SKILL.md +88 -47
  150. package/skills/ppr/SKILL.md +866 -379
  151. package/skills/prerender/SKILL.md +210 -108
  152. package/skills/rango/SKILL.md +115 -65
  153. package/skills/react-compiler/SKILL.md +6 -5
  154. package/skills/response-routes/SKILL.md +133 -25
  155. package/skills/route/SKILL.md +170 -60
  156. package/skills/router-setup/SKILL.md +420 -240
  157. package/skills/scripts/SKILL.md +39 -21
  158. package/skills/server-actions/SKILL.md +231 -59
  159. package/skills/shell-manifest/SKILL.md +28 -14
  160. package/skills/streams-and-websockets/SKILL.md +32 -14
  161. package/skills/tailwind/SKILL.md +37 -33
  162. package/skills/testing/SKILL.md +73 -33
  163. package/skills/testing/cache-prerender.md +60 -33
  164. package/skills/testing/client-components.md +59 -6
  165. package/skills/testing/e2e-parity.md +12 -2
  166. package/skills/testing/flight.md +19 -11
  167. package/skills/testing/handles.md +8 -5
  168. package/skills/testing/loader.md +26 -6
  169. package/skills/testing/middleware.md +8 -3
  170. package/skills/testing/predicates.md +120 -0
  171. package/skills/testing/render-handler.md +70 -22
  172. package/skills/testing/response-routes.md +11 -4
  173. package/skills/testing/reverse-and-types.md +20 -14
  174. package/skills/testing/serve-shell-request.md +188 -0
  175. package/skills/testing/server-actions.md +12 -3
  176. package/skills/testing/server-tree.md +9 -0
  177. package/skills/testing/setup.md +9 -5
  178. package/skills/theme/SKILL.md +155 -30
  179. package/skills/typesafety/SKILL.md +20 -9
  180. package/skills/typesafety/env-and-bindings.md +137 -50
  181. package/skills/typesafety/generated-files-and-cli.md +26 -14
  182. package/skills/typesafety/params-and-search.md +44 -19
  183. package/skills/typesafety/route-types.md +50 -14
  184. package/skills/use-cache/SKILL.md +224 -83
  185. package/skills/vercel/SKILL.md +35 -13
  186. package/skills/view-transitions/SKILL.md +42 -21
  187. package/src/__augment-tests__/augmented.check.ts +26 -1
  188. package/src/browser/cookie-name.ts +23 -0
  189. package/src/browser/history-state.ts +23 -0
  190. package/src/browser/index.ts +2 -2
  191. package/src/browser/navigation-bridge.ts +3 -4
  192. package/src/browser/navigation-transaction.ts +22 -2
  193. package/src/browser/partial-update.ts +35 -25
  194. package/src/browser/react/ScrollRestoration.tsx +6 -6
  195. package/src/browser/rsc-router.tsx +6 -1
  196. package/src/browser/scroll-restoration.ts +53 -6
  197. package/src/cache/cache-exec-scope.ts +37 -4
  198. package/src/cache/cache-key-utils.ts +23 -8
  199. package/src/cache/cache-policy.ts +22 -0
  200. package/src/cache/cache-runtime.ts +264 -68
  201. package/src/cache/cache-scope.ts +419 -43
  202. package/src/cache/cache-tag.ts +375 -11
  203. package/src/cache/cf/cf-base64.ts +36 -4
  204. package/src/cache/cf/cf-cache-constants.ts +12 -0
  205. package/src/cache/cf/cf-cache-store.ts +990 -348
  206. package/src/cache/cf/cf-cache-types.ts +32 -4
  207. package/src/cache/cf/cf-shell-frame.ts +258 -0
  208. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  209. package/src/cache/document-cache.ts +15 -0
  210. package/src/cache/handle-capture.ts +90 -10
  211. package/src/cache/handle-snapshot.ts +133 -28
  212. package/src/cache/index.ts +2 -0
  213. package/src/cache/isolate-tag-memo.ts +308 -0
  214. package/src/cache/segment-codec.ts +26 -7
  215. package/src/cache/shell-memo.ts +341 -0
  216. package/src/cache/shell-snapshot.ts +217 -27
  217. package/src/cache/tag-invalidation.ts +108 -22
  218. package/src/cache/taint.ts +1 -1
  219. package/src/cache/types.ts +225 -44
  220. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  221. package/src/client.rsc.tsx +10 -9
  222. package/src/client.tsx +5 -4
  223. package/src/components/DefaultDocument.tsx +7 -2
  224. package/src/context-var.ts +6 -5
  225. package/src/escape-script.ts +1 -1
  226. package/src/handles/MetaTags.tsx +13 -11
  227. package/src/handles/Scripts.tsx +11 -10
  228. package/src/handles/meta.ts +2 -2
  229. package/src/handles/script.ts +17 -16
  230. package/src/href-client.ts +1 -1
  231. package/src/html.ts +12 -0
  232. package/src/index.rsc.ts +5 -0
  233. package/src/index.ts +1 -0
  234. package/src/loader-store.ts +7 -4
  235. package/src/prerender/build-shell-capture.ts +73 -22
  236. package/src/reverse.ts +44 -4
  237. package/src/route-definition/dsl-helpers.ts +121 -33
  238. package/src/route-definition/helpers-types.ts +33 -9
  239. package/src/route-definition/resolve-handler-use.ts +121 -6
  240. package/src/route-types.ts +0 -3
  241. package/src/router/debug-manifest.ts +10 -2
  242. package/src/router/error-handling.ts +51 -15
  243. package/src/router/handler-context.ts +71 -28
  244. package/src/router/instrument.ts +54 -2
  245. package/src/router/intercept-resolution.ts +141 -43
  246. package/src/router/loader-resolution.ts +115 -39
  247. package/src/router/match-api.ts +40 -26
  248. package/src/router/match-handlers.ts +5 -0
  249. package/src/router/match-middleware/background-revalidation.ts +161 -96
  250. package/src/router/match-middleware/cache-lookup.ts +110 -10
  251. package/src/router/match-middleware/cache-store.ts +48 -101
  252. package/src/router/middleware-types.ts +9 -6
  253. package/src/router/middleware.ts +75 -13
  254. package/src/router/prerender-match.ts +59 -19
  255. package/src/router/route-snapshot.ts +11 -1
  256. package/src/router/router-context.ts +2 -0
  257. package/src/router/router-interfaces.ts +25 -2
  258. package/src/router/router-options.ts +8 -5
  259. package/src/router/segment-resolution/fresh.ts +62 -21
  260. package/src/router/segment-resolution/helpers.ts +4 -2
  261. package/src/router/segment-resolution/loader-cache.ts +407 -76
  262. package/src/router/segment-resolution/loader-mask.ts +12 -22
  263. package/src/router/segment-resolution/loader-snapshot.ts +208 -56
  264. package/src/router/segment-resolution/mask-nested.ts +138 -44
  265. package/src/router/segment-resolution/revalidation.ts +61 -16
  266. package/src/router/segment-wrappers.ts +4 -0
  267. package/src/rsc/full-payload.ts +25 -1
  268. package/src/rsc/handler.ts +14 -2
  269. package/src/rsc/progressive-enhancement.ts +3 -2
  270. package/src/rsc/render-pipeline.ts +3 -0
  271. package/src/rsc/response-cache-serve.ts +9 -6
  272. package/src/rsc/response-route-handler.ts +11 -3
  273. package/src/rsc/rsc-rendering.ts +608 -225
  274. package/src/rsc/shell-build-manifest.ts +27 -9
  275. package/src/rsc/shell-capture-constants.ts +73 -6
  276. package/src/rsc/shell-capture.ts +1068 -296
  277. package/src/rsc/shell-serve.ts +316 -34
  278. package/src/rsc/ssr-setup.ts +14 -8
  279. package/src/rsc/types.ts +18 -0
  280. package/src/server/context.ts +300 -32
  281. package/src/server/cookie-store.ts +87 -61
  282. package/src/server/handle-store.ts +232 -7
  283. package/src/server/request-context.ts +297 -67
  284. package/src/ssr/index.tsx +136 -44
  285. package/src/ssr/ssr-root.tsx +9 -8
  286. package/src/static-handler.ts +4 -1
  287. package/src/testing/flight-runtime.d.ts +21 -0
  288. package/src/testing/flight.entry.ts +14 -0
  289. package/src/testing/generated-routes.ts +2 -2
  290. package/src/testing/internal/context.ts +1 -1
  291. package/src/testing/internal/flight-client-globals.ts +7 -5
  292. package/src/testing/render-route.tsx +85 -24
  293. package/src/testing/run-loader.ts +68 -6
  294. package/src/testing/serve-shell-request.ts +378 -0
  295. package/src/testing/shell-status.ts +34 -12
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  297. package/src/testing/vitest.ts +62 -1
  298. package/src/theme/ThemeProvider.tsx +7 -6
  299. package/src/theme/ThemeScript.tsx +3 -3
  300. package/src/types/cache-types.ts +14 -10
  301. package/src/types/handler-context.ts +28 -1
  302. package/src/types/loader-types.ts +8 -6
  303. package/src/urls/pattern-types.ts +22 -18
  304. package/src/urls/response-types.ts +3 -23
  305. package/src/use-loader.tsx +12 -3
  306. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  307. package/src/vite/plugins/use-cache-transform.ts +118 -74
  308. package/src/vite/rango.ts +7 -3
  309. package/src/vite/router-discovery.ts +14 -4
@@ -6,8 +6,14 @@ argument-hint: [hook-name]
6
6
 
7
7
  # Client-Side React Hooks
8
8
 
9
+ The client-side API for reading router state in client components: navigation
10
+ state and actions, URL and params, loader data, handles, action status,
11
+ location state, outlets, and client-cache invalidation.
12
+
9
13
  Import the hooks and components in this skill from `@rangojs/router/client`.
10
- The root `@rangojs/router` entrypoint is for server/RSC APIs and shared types.
14
+ The root `@rangojs/router` entrypoint is for server/RSC APIs and shared types;
15
+ the exceptions used here are `invalidateClientCache()` (root only) and
16
+ `createLocationState()` (exported from both).
11
17
 
12
18
  ## Not this skill if…
13
19
 
@@ -16,6 +22,8 @@ The root `@rangojs/router` entrypoint is for server/RSC APIs and shared types.
16
22
  resolved.
17
23
  - You want to control when a loader re-runs after an action — that is
18
24
  `revalidate()` in the server DSL: see `/loader`.
25
+ - You want an action to leave the client cache alone — that is
26
+ `keepClientCache()` inside the action: see `/server-actions`.
19
27
 
20
28
  Each hook's full API and recipe lives in a companion file linked below. Read
21
29
  the one for your case.
@@ -65,22 +73,22 @@ the one for your case.
65
73
 
66
74
  ## Hook Summary
67
75
 
68
- | Hook | Purpose | Returns |
69
- | ------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------ |
70
- | `useParams()` | Route params | `Readonly<T>` (default `Record<string, string>`) or selected value |
71
- | `usePathname()` | Current pathname | `string` |
72
- | `useSearchParams()` | URL search params (read + write) | `[ReadonlyURLSearchParams, SetSearchParams]` (RR-style tuple) |
73
- | `useHref()` | Mount-aware href | `(path) => string` |
74
- | `useMount()` | Current include() mount path | `string` |
75
- | `useReverse()` | Local reverse for imported routes | `(name, params?, search?) => string` |
76
- | `useNavigation()` | Reactive navigation state | state, location, isStreaming |
77
- | `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
78
- | `useSegments()` | URL path & segment IDs | path, segmentIds, location |
79
- | `useLinkStatus()` | Link pending state | { pending } |
80
- | `useLoader()` | Loader data (strict) | data, isLoading, error, load, refetch |
81
- | `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading, error, refetch |
82
- | `useRefreshLoaders()` | Refresh cross-loader group(s) | `() => (groups: string \| string[]) => Promise<void>` |
83
- | `useHandle()` | Accumulated handle data | T (handle type) |
84
- | `useAction()` | Server action state | state, error, result |
85
- | `useLocationState()` | History state (persists or flash) | T \| undefined |
86
- | `invalidateClientCache()` | Force client caches to miss (function, not a hook; root entry) | `void` |
76
+ | Hook | Purpose | Returns |
77
+ | ------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------- |
78
+ | `useParams()` | Route params | `Readonly<T>` (default `Record<string, string \| undefined>`) or selected value |
79
+ | `usePathname()` | Current pathname | `string` |
80
+ | `useSearchParams()` | URL search params (read + write) | `[ReadonlyURLSearchParams, SetSearchParams]` (RR-style tuple) |
81
+ | `useHref()` | Mount-aware href | `(path) => string` |
82
+ | `useMount()` | Current include() mount path | `string` |
83
+ | `useReverse()` | Local reverse for imported routes | `(name, params?, search?) => string` |
84
+ | `useNavigation()` | Reactive navigation state | state, location, pendingUrl, isStreaming |
85
+ | `useRouter()` | Stable router actions | push, replace, refresh, prefetch, back, forward |
86
+ | `useSegments()` | URL path & segment IDs | path, segmentIds, location |
87
+ | `useLinkStatus()` | Link pending state | { pending } |
88
+ | `useLoader()` | Loader data (strict) | data, isLoading, error, load, refetch |
89
+ | `useFetchLoader()` | Loader with on-demand fetch | data, load, isLoading, error, refetch |
90
+ | `useRefreshLoaders()` | Refresh cross-loader group(s) | `() => (groups: string \| string[]) => Promise<void>` |
91
+ | `useHandle()` | Accumulated handle data | T (handle type) |
92
+ | `useAction()` | Server action state | state, actionId, payload, error, result |
93
+ | `useLocationState()` | History state (persists or flash) | T \| undefined |
94
+ | `invalidateClientCache()` | Force client caches to miss (function, not a hook; root entry) | `void` |
@@ -36,9 +36,15 @@ same-route nav, a same-structure search/filter nav), the reader that is still
36
36
  showing the OLD data reports `isLoading: true` from
37
37
  the moment the new tree is committed until the new data lands — render your
38
38
  stale indicator from it. The flag never flashes back to `false` on the old
39
- data: it flips in the same commit that swaps `data`. Only loaders the
40
- navigation actually re-runs are flagged: a layout loader the server does not
41
- re-execute keeps its data and stays `isLoading: false`. It never applies to a
39
+ data: it flips in the same commit that swaps `data`. The pin is per loader
40
+ family (`loader.$$id`), not per segment: a navigation that re-runs
41
+ `CartLoader` flags every `useLoader(CartLoader)`, including a persisting
42
+ layout reader whose own copy is not replaced. That is "this loader is in
43
+ flight," not "this segment's copy is being replaced." A _different_ layout
44
+ loader the server does not re-execute stays `isLoading: false`. A child can
45
+ already read a layout registration via the context walk; a second
46
+ `loader(Same)` on the child is a second run of the same family, not a
47
+ separate loading flag. It never applies to a
42
48
  navigation that remounts the route (that reader is gone; the skeleton shows),
43
49
  to a fully-prefetched nav (its data is already settled when it commits, so
44
50
  nothing is pending to flag), or to an ephemeral `useFetchLoader` read outside
@@ -51,8 +57,19 @@ function ProductPrice() {
51
57
  }
52
58
  ```
53
59
 
60
+ Unit-test the indicator with `renderRoute` (`/testing`, client-components.md
61
+ "Held navigation"): a `transition` spec plus a pending seed in
62
+ `router.navigate(url, { loaders })`.
63
+
54
64
  **Precondition**: Loader must be registered on route via `loader()` helper.
55
65
 
66
+ **`load()` / `refetch` need a fetchable loader.** `load()`, `refetch`, and
67
+ `useRefreshLoaders()` call the loader through the `_rsc_loader` endpoint, which
68
+ only serves loaders created with `createLoader(fn, true)` or
69
+ `createLoader(fn, { middleware })`. A plain `createLoader(fn)` answers 403 there;
70
+ it re-runs only as part of a render (navigation, `router.refresh()`, or the
71
+ revalidation after a server action). See `/loader` → "Fetchable Loaders".
72
+
56
73
  Loaders can also be passed as props from server to client components:
57
74
 
58
75
  ```tsx
@@ -108,6 +125,7 @@ reading the same loader id. Layout, page, and parallel-slot reads
108
125
  all converge on the new value:
109
126
 
110
127
  ```tsx
128
+ // CartLoader = createLoader(fn, true), registered with loader(CartLoader).
111
129
  // Layout button calls load() — the page read below sees the update too.
112
130
  function Layout() {
113
131
  const { data, load } = useLoader(CartLoader);
@@ -262,9 +280,11 @@ server, JSON bodies are available via `ctx.body` and FormData bodies via `ctx.fo
262
280
 
263
281
  ```tsx
264
282
  "use client";
283
+ import { useRef } from "react";
265
284
  import { useFetchLoader } from "@rangojs/router/client";
266
285
  import { FileUploadLoader } from "../loaders/upload";
267
286
 
287
+ // Needs JavaScript: the form action is a client function, not a server action.
268
288
  function FileUploader() {
269
289
  const { data, load, isLoading } = useFetchLoader(FileUploadLoader);
270
290
  const formRef = useRef<HTMLFormElement>(null);
@@ -34,18 +34,24 @@ Writers: handlers AND loader bodies — `ctx.use(Handle)` returns the push
34
34
  function in each (middleware has no handle APIs). Loader pushes follow a delivery race (settle before
35
35
  the handler barrier → in the SSR'd document; later → applied client-side), and
36
36
  `loader(Def, { ssr: false })` makes them deterministic in the
37
- document. See `/loader` → "Writing Handles from Loaders". `useHandle`
37
+ document. On a `ppr` shell HIT, a loader without `ssr: false` never ran during
38
+ the capture, so its pushes always apply client-side; with `ssr: false` they
39
+ bake into the shell. See `/loader` → "Writing Handles from Loaders". `useHandle`
38
40
  re-renders on every handle update, late loader pushes included.
39
41
 
40
42
  Handles can be passed as props from server to client components:
41
43
 
42
44
  ```tsx
43
- // Server component
44
- path("/dashboard", (ctx) => {
45
- const push = ctx.use(Breadcrumbs);
46
- push({ label: "Dashboard", href: "/dashboard" });
47
- return <DashboardNav handle={Breadcrumbs} />;
48
- });
45
+ // Server component (route handler)
46
+ path(
47
+ "/dashboard",
48
+ (ctx) => {
49
+ const push = ctx.use(Breadcrumbs);
50
+ push({ label: "Dashboard", href: "/dashboard" });
51
+ return <DashboardNav handle={Breadcrumbs} />;
52
+ },
53
+ { name: "dashboard" },
54
+ );
49
55
  ```
50
56
 
51
57
  ```tsx
@@ -58,7 +64,9 @@ function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
58
64
  return (
59
65
  <nav>
60
66
  {crumbs.map((c) => (
61
- <a href={c.href}>{c.label}</a>
67
+ <a key={c.href} href={c.href}>
68
+ {c.label}
69
+ </a>
62
70
  ))}
63
71
  </nav>
64
72
  );
@@ -78,33 +86,42 @@ tracking actions called outside a `<form action={...}>` flow.
78
86
 
79
87
  ### useAction()
80
88
 
81
- Track state of server action invocations:
89
+ Track the lifecycle of a server action, wherever on the page it was invoked.
90
+ If the same action fires several times, the hook reports the latest call.
82
91
 
83
92
  ```tsx
84
93
  "use client";
85
94
  import { useAction } from "@rangojs/router/client";
86
- import { addToCart } from "../actions/cart";
95
+ import { addToCart } from "../actions/cart"; // addToCart(productId: string)
87
96
 
88
97
  function AddToCartButton({ productId }: { productId: string }) {
89
98
  const { state, error, result } = useAction(addToCart);
90
99
 
91
100
  // state: 'idle' | 'loading' | 'streaming'
92
101
  // actionId: string | null
93
- // payload: unknown | null (input data)
94
- // error: Error | null
102
+ // payload: unknown[] | FormData | null (the call's arguments)
103
+ // error: unknown | null (whatever the action threw)
95
104
  // result: unknown | null (return value)
96
105
 
97
106
  return (
98
- <form action={addToCart}>
99
- <input type="hidden" name="productId" value={productId} />
107
+ // .bind keeps the form progressively enhanced; useAction still matches it
108
+ <form action={addToCart.bind(null, productId)}>
100
109
  <button disabled={state === "loading"}>
101
110
  {state === "loading" ? "Adding..." : "Add to Cart"}
102
111
  </button>
103
- {error && <p className="error">{error.message}</p>}
112
+ {error instanceof Error && <p className="error">{error.message}</p>}
104
113
  </form>
105
114
  );
106
115
  }
107
116
 
108
- // Match by string suffix (convenient but may be ambiguous)
109
- const isLoading = useAction("addToCart", (s) => s.state === "loading");
117
+ // With a selector (re-renders only when the selected value changes)
118
+ const isLoading = useAction(addToCart, (s) => s.state === "loading");
119
+
120
+ // Match by export name (suffix match on "#addToCart"; ambiguous if two
121
+ // "use server" files export the same name)
122
+ const isAdding = useAction("addToCart", (s) => s.state === "loading");
110
123
  ```
124
+
125
+ Pass an action imported directly into the client component, or its export
126
+ name as a string. An action received as a prop from a server component has
127
+ lost the metadata `useAction` needs and throws; use the string form there.
@@ -13,7 +13,7 @@ function NavIndicator() {
13
13
 
14
14
  // State properties
15
15
  nav.state; // 'idle' | 'loading'
16
- nav.isStreaming; // boolean
16
+ nav.isStreaming; // boolean — RSC data is still streaming
17
17
  nav.location; // Current URL
18
18
  nav.pendingUrl; // Target URL during navigation (or null)
19
19
 
@@ -29,7 +29,9 @@ function IsLoading() {
29
29
 
30
30
  ### useRouter()
31
31
 
32
- Access stable router actions (never causes re-renders):
32
+ Access stable router actions. The returned object never changes identity, so
33
+ components using it do not re-render on navigation. Call its methods from
34
+ event handlers or effects:
33
35
 
34
36
  ```tsx
35
37
  "use client";
@@ -38,15 +40,41 @@ import { useRouter } from "@rangojs/router/client";
38
40
  function NavigationControls() {
39
41
  const router = useRouter();
40
42
 
41
- router.push("/products"); // Navigate (adds history entry)
42
- router.replace("/login"); // Navigate (replaces history entry)
43
- router.refresh(); // Re-fetch current route data
44
- router.prefetch("/dashboard"); // Prefetch for faster navigation
45
- router.back(); // Go back in history
46
- router.forward(); // Go forward in history
43
+ return (
44
+ <>
45
+ <button onClick={() => router.push("/products")}>Products</button>
46
+ <button onClick={() => router.replace("/login", { scroll: false })}>
47
+ Log in
48
+ </button>
49
+ <button onClick={() => router.refresh()}>Reload data</button>
50
+ <button onMouseEnter={() => router.prefetch("/dashboard")}>
51
+ Dashboard
52
+ </button>
53
+ <button onClick={() => router.back()}>Back</button>
54
+ <button onClick={() => router.forward()}>Forward</button>
55
+ </>
56
+ );
47
57
  }
48
58
  ```
49
59
 
60
+ | Method | Behaviour |
61
+ | ------------------------ | --------------------------------------------------------------------------------------------------------------- |
62
+ | `push(url, options?)` | Navigate and add a history entry. Returns a promise. |
63
+ | `replace(url, options?)` | Navigate and replace the current history entry. Returns a promise. |
64
+ | `refresh()` | Re-fetch the current route's server data, keeping client state. Returns a promise. |
65
+ | `prefetch(url, opts?)` | Prefetch a target. `{ key: ":source" }` scopes the entry to the source page (parity with `<Link prefetchKey>`). |
66
+ | `back()` | `history.back()`; on the first entry of the session it replaces to the app root instead of leaving the app. |
67
+ | `forward()` | `history.forward()`. |
68
+
69
+ `push` / `replace` options: `scroll` (`false` keeps the current scroll
70
+ position), `revalidate` (default `true`, see below), and `state` (location
71
+ state, see [`./state.md`](./state.md)).
72
+
73
+ Target resolution: a path starting with `/` is app-absolute and gets the
74
+ router `basename` prefixed. A relative path (`"cart"`, `"./cart"`) resolves
75
+ against the current `include()` mount, so `router.push("cart")` inside
76
+ `include("/shop", ...)` navigates to `/shop/cart`.
77
+
50
78
  #### Skipping revalidation
51
79
 
52
80
  Pass `revalidate: false` to skip the RSC server fetch for same-pathname navigations (search param or hash changes). The URL updates and all hooks re-render, but server components stay as-is.
@@ -79,7 +107,7 @@ function Breadcrumbs() {
79
107
  const { path, segmentIds, location } = useSegments();
80
108
 
81
109
  // path: ["shop", "products", "123"] (split on "/", no leading slash on any element)
82
- // segmentIds: ["L0", "L0L1", "L0L1R0"] (opaque internal short-codes, not route names)
110
+ // segmentIds: ["L0", "L0L1", "L0L1R0"] (layouts and routes only; opaque internal short-codes, not route names)
83
111
  // location: URL object
84
112
 
85
113
  return <nav>{path.join(" > ")}</nav>;
@@ -91,7 +119,8 @@ const isShopRoute = useSegments((s) => s.path[0] === "shop");
91
119
 
92
120
  ### useLinkStatus()
93
121
 
94
- Track pending state inside a Link component:
122
+ Track pending state inside a Link component. `pending` is `true` while a
123
+ navigation to this link's `to` is in flight:
95
124
 
96
125
  ```tsx
97
126
  "use client";
@@ -2,24 +2,32 @@
2
2
 
3
3
  ### Outlet / ParallelOutlet
4
4
 
5
- Render child content in layouts:
5
+ Render child content in layouts. `<Outlet />` renders the matched child
6
+ segment; `<ParallelOutlet name="@slot" />` renders a named `parallel()` slot.
7
+ Both apply the segment's `loading()` fallback as a Suspense boundary:
6
8
 
7
9
  ```tsx
8
10
  import { Outlet, ParallelOutlet } from "@rangojs/router/client";
9
11
 
10
- function DashboardLayout({ children }: { children?: React.ReactNode }) {
12
+ function DashboardLayout() {
11
13
  return (
12
14
  <div className="dashboard">
13
15
  <aside>
14
16
  <ParallelOutlet name="@sidebar" />
15
17
  </aside>
16
- <main>{children ?? <Outlet />}</main>
18
+ <main>
19
+ <Outlet />
20
+ </main>
17
21
  <ParallelOutlet name="@notifications" />
18
22
  </div>
19
23
  );
20
24
  }
21
25
  ```
22
26
 
27
+ `<Outlet name="@sidebar" />` is equivalent to `<ParallelOutlet name="@sidebar" />`;
28
+ `ParallelOutlet` requires the name and reads clearer at the call site. See
29
+ `/layout` and `/parallel` for the server side.
30
+
23
31
  ### useOutlet()
24
32
 
25
33
  Access outlet content and its client-route presentation state programmatically:
@@ -48,9 +56,8 @@ function ConditionalLayout() {
48
56
  }
49
57
  ```
50
58
 
51
- Migration from the old bare-node return is `const { content, pending } =
52
- useOutlet()`, then render `content` where you previously rendered the hook result.
53
- `<Outlet />` itself is unchanged.
59
+ Migrating from the older API where `useOutlet()` returned the node directly:
60
+ destructure `content` and render it where you rendered the hook result.
54
61
 
55
62
  `pending` is narrow. After hydration, a `clientUrls()` layout receives `true`
56
63
  while a browser-local match to a different client route beneath it is presenting
@@ -74,6 +74,16 @@ Plain state can also be evaluated just in time (also requires a client component
74
74
  </Link>
75
75
  ```
76
76
 
77
+ Read plain (untyped) state by calling the hook with a type argument and no
78
+ definition:
79
+
80
+ ```tsx
81
+ const state = useLocationState<{ from?: string }>(); // { from?: string } | undefined
82
+ ```
83
+
84
+ The same `state` option exists on `router.push()` / `router.replace()` (see
85
+ [`./navigation.md`](./navigation.md)).
86
+
77
87
  ### Flash State (read-once)
78
88
 
79
89
  Create a location state with `{ flash: true }` for read-once state that
@@ -106,22 +116,22 @@ function FlashBanner() {
106
116
  ```
107
117
 
108
118
  Flash behavior is determined by the definition (`{ flash: true }`), not by which
109
- hook reads it. `useLocationState` reads the value synchronously during render,
110
- then clears it from `history.state` via `replaceState` in a `useEffect`.
111
- Multiple components reading the same flash definition all see the value.
112
- Pressing back/forward will not re-show the flash since it was cleared.
119
+ hook reads it. `useLocationState` reads the value during render (on the
120
+ hydration render it returns `undefined` and reads in a post-mount effect, so
121
+ SSR and hydration agree), then clears it from `history.state` via
122
+ `replaceState` in a `useEffect`. Multiple components reading the same flash
123
+ definition all see the value. Pressing back/forward will not re-show the flash
124
+ since it was cleared.
113
125
 
114
- Set flash state from the server via `redirect()` with state:
126
+ Set flash state from the server via `redirect()` with state (from a handler,
127
+ middleware, or server action). Import the definition from the shared module so
128
+ the client reader uses the same one:
115
129
 
116
130
  ```tsx
117
131
  // In a route handler
118
- import { redirect, createLocationState } from "@rangojs/router";
119
-
120
- export const FlashMessage = createLocationState<{ text: string }>({
121
- flash: true,
122
- });
132
+ import { redirect } from "@rangojs/router";
133
+ import { FlashMessage } from "../location-states";
123
134
 
124
- // Handler
125
135
  (ctx) => {
126
136
  return redirect("/dashboard", {
127
137
  state: [FlashMessage({ text: "Item saved!" })],
@@ -129,7 +139,7 @@ export const FlashMessage = createLocationState<{ text: string }>({
129
139
  };
130
140
  ```
131
141
 
132
- Or via `ctx.setLocationState()` on any response:
142
+ Or via `ctx.setLocationState()` on any response (handlers and middleware):
133
143
 
134
144
  ```tsx
135
145
  (ctx) => {
@@ -194,9 +204,23 @@ ProductState.delete();
194
204
  Force the client's caches to miss after a mutation the router can't see (a REST
195
205
  call, a WebSocket push, a login). It is a plain function, not a hook, so it works
196
206
  from module-level callbacks too. Imported from the root entry `@rangojs/router`,
197
- it is selected by export conditions: in a client component it marks the caches
198
- stale immediately; from a handler/server component it writes a rotated
199
- `Set-Cookie` for the responding client.
207
+ it is selected by export conditions:
208
+
209
+ - **Client** (client component or module): marks the history cache stale,
210
+ flushes the prefetch cache, rotates the router state, and notifies other tabs
211
+ — the same thing a completed server action does. During SSR of a client
212
+ component it is a no-op (dev warning).
213
+ - **Server** (handler, server component, middleware, loader, or action): writes
214
+ a rotated state `Set-Cookie` for the responding client, which marks its
215
+ caches stale when it next reads them. Idempotent within a request; throws
216
+ inside a `cache()` / `"use cache"` boundary; a no-op with a dev warning
217
+ outside a request. On a `ppr` route a shell HIT runs no handler, so a call
218
+ from handler code writes the cookie only on the renders that run it (a
219
+ MISS); call it from middleware, which runs on every request, HITs included.
220
+
221
+ Server actions already invalidate automatically; to suppress that for a no-op
222
+ action, call `keepClientCache()` inside it (see `/server-actions` → "Client
223
+ Cache After an Action").
200
224
 
201
225
  ```tsx
202
226
  "use client";
@@ -29,7 +29,7 @@ function ProductId() {
29
29
  }
30
30
  ```
31
31
 
32
- Returns merged params from all matched route segments as a `Readonly<T>` map. Updates on navigation commit (not during pending navigation).
32
+ Returns merged params from all matched route segments as a `Readonly<T>` map (default `Record<string, string | undefined>` — absent optional params are omitted, so guard them). Updates on navigation commit, not during a pending navigation — except inside an optimistically rendered `clientUrls()` destination, where it reports that destination's params (see `/client-urls`).
33
33
 
34
34
  ### usePathname()
35
35
 
@@ -119,19 +119,21 @@ import { useHref, href, Link } from "@rangojs/router/client";
119
119
 
120
120
  // Inside include("/shop", shopPatterns)
121
121
  function ShopNav() {
122
- const href = useHref();
122
+ const localHref = useHref();
123
123
 
124
124
  return (
125
125
  <>
126
126
  {/* Local paths - auto-prefixed with /shop */}
127
- <Link to={href("/cart")}>Cart</Link>
128
- <Link to={href("/product/widget")}>Widget</Link>
127
+ <Link to={localHref("/cart")}>Cart</Link>
128
+ <Link to={localHref("/product/widget")}>Widget</Link>
129
+ {/* Absolute path - not prefixed */}
130
+ <Link to={href("/about")}>About</Link>
129
131
  </>
130
132
  );
131
133
  }
132
134
  ```
133
135
 
134
- Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths.
136
+ Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths. The function `useHref()` returns is referentially stable within a mount, so it is safe as a hook dependency or memoized prop.
135
137
 
136
138
  ### useMount()
137
139
 
@@ -8,6 +8,12 @@ argument-hint:
8
8
 
9
9
  Route requests to different apps based on domain, subdomain, or path prefix patterns. Supports middleware, lazy loading, cookie-based host override for dev, and a fallback handler.
10
10
 
11
+ Use it when one deployment serves several independently built Rango apps (or
12
+ plain request handlers) and the hostname or a path prefix decides which one
13
+ handles the request. Each sub-app is an ordinary `createRouter()` app; the host
14
+ router only picks which one runs. For a single app mounted under a sub-path,
15
+ use `basename` instead (`/router-setup`).
16
+
11
17
  ## Import
12
18
 
13
19
  ```typescript
@@ -64,6 +70,10 @@ import { router } from "./router.js";
64
70
  export default (request: Request, input: any) => router.fetch(request, input);
65
71
  ```
66
72
 
73
+ Each sub-app has its own generated route map and `Rango.Env`; typecheck each
74
+ in its own TypeScript program (or give route names per-app prefixes) so the
75
+ global augmentations don't collide — see `/typesafety` (multi-project setup).
76
+
67
77
  Selecting the host entry — a host app has several `createRouter()` sub-apps, so single-router auto-discovery can't pick one. Either let rango auto-detect the lone `createHostRouter()` file, or point at it explicitly:
68
78
 
69
79
  ```typescript
@@ -71,7 +81,7 @@ Selecting the host entry — a host app has several `createRouter()` sub-apps, s
71
81
  rango({ preset: "vercel", hostRouter: "./src/worker.rsc.tsx" });
72
82
  ```
73
83
 
74
- On Vercel this is a single function running `hostRouter.match()` for every request (mirrors the Cloudflare single-worker model); `{ env, ctx }` (`process.env` + `{ waitUntil }`) is threaded unchanged to each matched sub-app's handler and `cache(env, ctx)` factory. See the `vercel` skill.
84
+ On Vercel this is a single function running `hostRouter.match()` for every request (mirrors the Cloudflare single-worker model); `{ env, ctx }` (`process.env` + `{ waitUntil }`) is threaded unchanged to each matched sub-app's handler and `cache(env, ctx)` factory. See `/vercel`.
75
85
 
76
86
  Unmatched hosts on node/vercel: because rango owns the generated entry (you have no worker `try/catch`), it catches `NoRouteMatchError` and returns **404** by default — so you do **not** need a catch-all host route. If you want different behavior (a branded 404, a redirect, a default app), register a catch-all mount as the **last** route, e.g. `host(["**"]).lazy(() => import("./apps/site/handler.js"))` — it matches any host, so the built-in 404 only fires when nothing matched at all. (Note `fallback()` is for cookie-override errors, not general unmatched hosts.)
77
87
 
@@ -120,6 +130,18 @@ Why two methods instead of one overloaded `.map()`:
120
130
 
121
131
  Patterns are tested in registration order. First match wins.
122
132
 
133
+ Matching details:
134
+
135
+ - **Apex means exactly two labels.** There is no Public Suffix List:
136
+ `example.co.uk` has three labels, so `.`/`*` do not match it and `*.` does.
137
+ `localhost` has one label, so it matches neither `.` nor `*.`; use an exact
138
+ `localhost` pattern (or `**`) for local dev.
139
+ - **Hosts are case-insensitive; paths are not.** A path prefix matches the
140
+ exact path or anything below it (`example.com/api` matches `"/api"` and
141
+ `"/api/x"`, not `"/apix"`). A trailing `/` on the pattern is ignored.
142
+ - A pattern that is empty, not a string, or contains whitespace throws
143
+ `InvalidPatternError` at registration.
144
+
123
145
  ## `defineHosts` for Type Safety
124
146
 
125
147
  ```typescript
@@ -142,6 +164,8 @@ Returns a frozen object — keys are autocompleted by TypeScript.
142
164
  Global middleware runs for every matched route. Per-route middleware runs only for that host pattern.
143
165
 
144
166
  ```typescript
167
+ import { createHostRouter, type Middleware } from "@rangojs/router/host";
168
+
145
169
  const router = createHostRouter();
146
170
 
147
171
  // Global — runs for all routes
@@ -150,7 +174,14 @@ router.use(async (request, input, next) => {
150
174
  return next();
151
175
  });
152
176
 
153
- // Per-route
177
+ // Per-route: return a Response without calling next() to short-circuit
178
+ const requireAuth: Middleware = async (request, input, next) => {
179
+ if (!request.headers.get("authorization")) {
180
+ return new Response("Unauthorized", { status: 401 });
181
+ }
182
+ return next();
183
+ };
184
+
154
185
  router
155
186
  .host(["admin.*"])
156
187
  .use(requireAuth)
@@ -159,23 +190,37 @@ router
159
190
 
160
191
  Middleware signature: `(request: Request, input: RouterRequestInput, next: () => Promise<Response>) => Promise<Response>`
161
192
 
193
+ Global middleware runs first, then the matched route's middleware, then the
194
+ handler. `input` is typed `RouterRequestInput<any>` on purpose: one host router
195
+ fans out to sub-apps with different env shapes, so `input.env`/`input.vars` are
196
+ `any` here and each sub-app types its own env via `createRouter<TEnv>()`.
197
+
162
198
  Calling `next()` more than once throws.
163
199
 
164
200
  ## Fallback Handler
165
201
 
166
- Handles cookie-override errors when `hostOverride` is configured (e.g., override from a disallowed host, invalid cookie hostname). The fallback does **not** catch unmatched hosts — those throw `NoRouteMatchError`. Catch that at the worker level if you need a 404.
202
+ Handles cookie-override errors when `hostOverride` is configured (e.g., override from a disallowed host, invalid cookie hostname, a throwing `validate`). The fallback does **not** catch unmatched hosts — those throw `NoRouteMatchError`. Catch that at the worker level if you need a 404.
167
203
 
168
204
  ```typescript
169
205
  const router = createHostRouter({
170
206
  hostOverride: { cookieName: "x-dev-host", allowedHosts: ["localhost"] },
171
207
  });
172
208
 
173
- // Called when cookie override fails (not for general unmatched hosts)
174
- router.fallback().map((request) => {
175
- return new Response("Invalid host override", { status: 400 });
209
+ // Called when cookie override fails (not for general unmatched hosts).
210
+ // Global middleware and the fallback's own middleware still run.
211
+ router.fallback().map((request, input) => {
212
+ // The override error is passed on the input (not part of RouterRequestInput's type)
213
+ const { error } = input as { error?: Error };
214
+ return new Response(`Invalid host override: ${error?.message}`, {
215
+ status: 400,
216
+ });
176
217
  });
177
218
  ```
178
219
 
220
+ Without a registered fallback, a failed override returns a `400` JSON
221
+ response (`{ error, message }`) that also clears the override cookie
222
+ (`Set-Cookie: <cookieName>=; Max-Age=0`).
223
+
179
224
  For unmatched hosts without `hostOverride`, catch `NoRouteMatchError` in your worker fetch. Use the `isNoRouteMatchError()` guard rather than a bare `instanceof`: a workspace with a duplicated `@rangojs/router` copy can throw the error with a different class identity, and `instanceof` would then turn the 404 into an opaque 500.
180
225
 
181
226
  ```typescript
@@ -215,10 +260,14 @@ const router = createHostRouter({
215
260
  When a request arrives:
216
261
 
217
262
  1. If no cookie → use actual hostname
218
- 2. If cookie present and host is in `allowedHosts` → use cookie value as hostname
219
- 3. If cookie present but host not allowed → throw `HostOverrideNotAllowedError`
263
+ 2. If cookie present and the actual host matches an `allowedHosts` pattern → use the cookie value as the hostname (after `validate`, when given)
264
+ 3. If cookie present but the host is not allowed → `HostOverrideNotAllowedError`, handled by the fallback (see above)
220
265
 
221
- Without a custom `validate`, the cookie value is validated as a hostname via `new URL()`.
266
+ Without a custom `validate`, the cookie value must parse as a bare hostname via
267
+ `new URL()` (no port or path) and is lowercased; otherwise
268
+ `InvalidHostnameError`. A custom `validate` returns the effective hostname; if
269
+ it throws, the error is wrapped in `HostValidationError`. The effective
270
+ hostname is then matched against the host patterns as usual.
222
271
 
223
272
  ## Debug Mode
224
273
 
@@ -251,12 +300,15 @@ matchesHost(
251
300
  new Request("https://foo.workers.dev/admin"),
252
301
  ); // true
253
302
 
254
- // Create requests for integration tests
303
+ // Create requests for integration tests (method defaults to GET, path to "/")
255
304
  const request = createTestRequest({
256
305
  host: "admin.example.com",
257
306
  path: "/dashboard",
307
+ method: "POST",
308
+ headers: { authorization: "Bearer test" },
258
309
  cookies: { "x-dev-host": "api.example.com" },
259
310
  });
311
+ const response = await router.match(request);
260
312
 
261
313
  // Test which route would match (without executing)
262
314
  router.test("admin.example.com"); // { pattern, handler, kind } | null
@@ -309,8 +361,8 @@ document navigation**, not a soft in-tree swap. When the server sees a partial
309
361
  Why a reload rather than a soft swap: a soft swap can't faithfully re-establish
310
362
  the target app's **document-level** state. Stylesheets shared across apps are
311
363
  dropped by React 19's by-`href` resource dedup; and theme, warmup, and
312
- prefetch-TTL are document-lifetime (captured once at load — see
313
- `browser/app-shell.ts`), so the target app's config would never take effect. A
364
+ prefetch-TTL are document-lifetime (captured once at document load), so the
365
+ target app's config would never take effect. A
314
366
  full document load re-establishes the target app's entire document — CSS, theme,
315
367
  meta, everything — by construction. So you do **not** need to coordinate
316
368
  stylesheet `href`s, `precedence`, theme config, etc. across independently-authored