@rangojs/router 0.15.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/README.md +254 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  6. package/dist/types/browser/rsc-router.d.ts +1 -1
  7. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  8. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  9. package/dist/types/cache/cache-scope.d.ts +34 -5
  10. package/dist/types/cache/handle-capture.d.ts +41 -3
  11. package/dist/types/cache/handle-snapshot.d.ts +53 -13
  12. package/dist/types/cache/segment-codec.d.ts +10 -3
  13. package/dist/types/cache/taint.d.ts +1 -1
  14. package/dist/types/cache/types.d.ts +15 -3
  15. package/dist/types/client.d.ts +4 -4
  16. package/dist/types/client.rsc.d.ts +4 -5
  17. package/dist/types/components/DefaultDocument.d.ts +5 -2
  18. package/dist/types/context-var.d.ts +6 -5
  19. package/dist/types/escape-script.d.ts +1 -1
  20. package/dist/types/handles/MetaTags.d.ts +5 -4
  21. package/dist/types/handles/Scripts.d.ts +8 -7
  22. package/dist/types/handles/meta.d.ts +2 -2
  23. package/dist/types/handles/script.d.ts +17 -16
  24. package/dist/types/href-client.d.ts +1 -1
  25. package/dist/types/html.d.ts +12 -0
  26. package/dist/types/index.d.ts +1 -1
  27. package/dist/types/index.rsc.d.ts +3 -2
  28. package/dist/types/loader-store.d.ts +52 -0
  29. package/dist/types/reverse.d.ts +36 -4
  30. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  31. package/dist/types/route-definition/helpers-types.d.ts +36 -10
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +3 -1
  34. package/dist/types/router/debug-manifest.d.ts +3 -2
  35. package/dist/types/router/error-handling.d.ts +20 -0
  36. package/dist/types/router/handler-context.d.ts +6 -0
  37. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  38. package/dist/types/router/middleware-types.d.ts +3 -2
  39. package/dist/types/router/router-context.d.ts +3 -1
  40. package/dist/types/router/router-interfaces.d.ts +24 -2
  41. package/dist/types/router/router-options.d.ts +8 -5
  42. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  43. package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
  44. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  45. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
  46. package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
  47. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  48. package/dist/types/router/segment-wrappers.d.ts +1 -1
  49. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  50. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  51. package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
  52. package/dist/types/rsc/shell-capture.d.ts +5 -4
  53. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  54. package/dist/types/rsc/types.d.ts +10 -0
  55. package/dist/types/server/context.d.ts +33 -3
  56. package/dist/types/server/handle-store.d.ts +26 -2
  57. package/dist/types/server/request-context.d.ts +24 -21
  58. package/dist/types/ssr/index.d.ts +15 -0
  59. package/dist/types/static-handler.d.ts +4 -1
  60. package/dist/types/testing/generated-routes.d.ts +2 -2
  61. package/dist/types/testing/internal/context.d.ts +1 -1
  62. package/dist/types/testing/render-route.d.ts +32 -6
  63. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  64. package/dist/types/testing/vitest.d.ts +35 -1
  65. package/dist/types/theme/ThemeScript.d.ts +3 -3
  66. package/dist/types/thenable-status.d.ts +9 -0
  67. package/dist/types/types/cache-types.d.ts +14 -10
  68. package/dist/types/types/handler-context.d.ts +14 -0
  69. package/dist/types/types/loader-types.d.ts +8 -6
  70. package/dist/types/urls/response-types.d.ts +3 -12
  71. package/dist/types/use-loader.d.ts +12 -3
  72. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  73. package/dist/types/vite/rango.d.ts +1 -1
  74. package/dist/vite/index.js +884 -768
  75. package/package.json +4 -4
  76. package/skills/api-client/SKILL.md +22 -10
  77. package/skills/breadcrumbs/SKILL.md +65 -22
  78. package/skills/bundle-analysis/SKILL.md +22 -22
  79. package/skills/cache-guide/SKILL.md +207 -97
  80. package/skills/caching/SKILL.md +295 -93
  81. package/skills/catalog.json +16 -16
  82. package/skills/client-urls/SKILL.md +53 -31
  83. package/skills/cloudflare/SKILL.md +72 -20
  84. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  85. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  86. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  87. package/skills/comparison/SKILL.md +5 -0
  88. package/skills/comparison/references/framework-comparison.md +39 -33
  89. package/skills/composability/SKILL.md +38 -29
  90. package/skills/css/SKILL.md +10 -13
  91. package/skills/debug-manifest/SKILL.md +74 -31
  92. package/skills/defer-hydration/SKILL.md +2 -1
  93. package/skills/deployment-caching/SKILL.md +6 -1
  94. package/skills/document-cache/SKILL.md +92 -28
  95. package/skills/fonts/SKILL.md +16 -8
  96. package/skills/handler-use/SKILL.md +53 -17
  97. package/skills/hooks/SKILL.md +28 -20
  98. package/skills/hooks/data.md +43 -1
  99. package/skills/hooks/handle-and-actions.md +31 -16
  100. package/skills/hooks/navigation.md +39 -10
  101. package/skills/hooks/outlets.md +13 -6
  102. package/skills/hooks/state.md +37 -15
  103. package/skills/hooks/urls.md +7 -5
  104. package/skills/host-router/SKILL.md +64 -12
  105. package/skills/i18n/SKILL.md +60 -34
  106. package/skills/intercept/SKILL.md +161 -26
  107. package/skills/layout/SKILL.md +106 -50
  108. package/skills/links/SKILL.md +51 -19
  109. package/skills/loader/SKILL.md +242 -106
  110. package/skills/middleware/SKILL.md +119 -23
  111. package/skills/migrate-nextjs/SKILL.md +100 -45
  112. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  113. package/skills/migrate-react-router/SKILL.md +49 -38
  114. package/skills/migrate-react-router/component-migration.md +25 -6
  115. package/skills/migrate-react-router/data-and-actions.md +18 -5
  116. package/skills/migrate-react-router/route-mapping.md +12 -6
  117. package/skills/mime-routes/SKILL.md +12 -7
  118. package/skills/observability/SKILL.md +28 -14
  119. package/skills/parallel/SKILL.md +75 -39
  120. package/skills/ppr/SKILL.md +345 -150
  121. package/skills/prerender/SKILL.md +189 -100
  122. package/skills/rango/SKILL.md +65 -34
  123. package/skills/react-compiler/SKILL.md +6 -5
  124. package/skills/response-routes/SKILL.md +133 -25
  125. package/skills/route/SKILL.md +165 -60
  126. package/skills/router-setup/SKILL.md +418 -240
  127. package/skills/scripts/SKILL.md +31 -20
  128. package/skills/server-actions/SKILL.md +220 -59
  129. package/skills/shell-manifest/SKILL.md +12 -7
  130. package/skills/streams-and-websockets/SKILL.md +32 -14
  131. package/skills/tailwind/SKILL.md +37 -33
  132. package/skills/testing/SKILL.md +68 -32
  133. package/skills/testing/cache-prerender.md +55 -29
  134. package/skills/testing/client-components.md +59 -6
  135. package/skills/testing/e2e-parity.md +12 -2
  136. package/skills/testing/flight.md +19 -11
  137. package/skills/testing/handles.md +8 -5
  138. package/skills/testing/loader.md +8 -5
  139. package/skills/testing/middleware.md +8 -3
  140. package/skills/testing/predicates.md +120 -0
  141. package/skills/testing/render-handler.md +70 -22
  142. package/skills/testing/response-routes.md +11 -4
  143. package/skills/testing/reverse-and-types.md +20 -14
  144. package/skills/testing/server-actions.md +12 -3
  145. package/skills/testing/server-tree.md +9 -0
  146. package/skills/testing/setup.md +9 -5
  147. package/skills/theme/SKILL.md +106 -29
  148. package/skills/typesafety/SKILL.md +20 -9
  149. package/skills/typesafety/env-and-bindings.md +126 -47
  150. package/skills/typesafety/generated-files-and-cli.md +26 -14
  151. package/skills/typesafety/params-and-search.md +44 -19
  152. package/skills/typesafety/route-types.md +50 -14
  153. package/skills/use-cache/SKILL.md +200 -82
  154. package/skills/vercel/SKILL.md +21 -11
  155. package/skills/view-transitions/SKILL.md +41 -20
  156. package/src/__augment-tests__/augmented.check.ts +26 -1
  157. package/src/browser/index.ts +2 -2
  158. package/src/browser/navigation-bridge.ts +3 -4
  159. package/src/browser/navigation-transaction.ts +22 -2
  160. package/src/browser/partial-update.ts +75 -80
  161. package/src/browser/react/ScrollRestoration.tsx +6 -6
  162. package/src/browser/rsc-router.tsx +1 -1
  163. package/src/browser/scroll-restoration.ts +53 -6
  164. package/src/cache/cache-exec-scope.ts +37 -4
  165. package/src/cache/cache-key-utils.ts +1 -6
  166. package/src/cache/cache-runtime.ts +230 -61
  167. package/src/cache/cache-scope.ts +76 -12
  168. package/src/cache/document-cache.ts +15 -0
  169. package/src/cache/handle-capture.ts +90 -10
  170. package/src/cache/handle-snapshot.ts +132 -28
  171. package/src/cache/segment-codec.ts +26 -7
  172. package/src/cache/tag-invalidation.ts +4 -3
  173. package/src/cache/taint.ts +1 -1
  174. package/src/cache/types.ts +16 -3
  175. package/src/client.rsc.tsx +10 -9
  176. package/src/client.tsx +5 -4
  177. package/src/components/DefaultDocument.tsx +7 -2
  178. package/src/context-var.ts +6 -5
  179. package/src/escape-script.ts +1 -1
  180. package/src/handles/MetaTags.tsx +13 -11
  181. package/src/handles/Scripts.tsx +11 -10
  182. package/src/handles/meta.ts +2 -2
  183. package/src/handles/script.ts +17 -16
  184. package/src/href-client.ts +1 -1
  185. package/src/html.ts +12 -0
  186. package/src/index.rsc.ts +5 -0
  187. package/src/index.ts +1 -0
  188. package/src/loader-store.ts +106 -0
  189. package/src/prerender/build-shell-capture.ts +8 -11
  190. package/src/reverse.ts +44 -4
  191. package/src/route-definition/dsl-helpers.ts +134 -42
  192. package/src/route-definition/helpers-types.ts +36 -10
  193. package/src/route-definition/resolve-handler-use.ts +121 -6
  194. package/src/route-types.ts +2 -3
  195. package/src/router/debug-manifest.ts +10 -2
  196. package/src/router/error-handling.ts +51 -15
  197. package/src/router/handler-context.ts +34 -25
  198. package/src/router/intercept-resolution.ts +125 -39
  199. package/src/router/loader-resolution.ts +56 -11
  200. package/src/router/match-api.ts +12 -17
  201. package/src/router/match-middleware/background-revalidation.ts +161 -96
  202. package/src/router/match-middleware/cache-lookup.ts +93 -10
  203. package/src/router/match-middleware/cache-store.ts +17 -90
  204. package/src/router/middleware-types.ts +3 -5
  205. package/src/router/middleware.ts +52 -9
  206. package/src/router/prerender-match.ts +53 -19
  207. package/src/router/route-snapshot.ts +11 -1
  208. package/src/router/router-context.ts +2 -0
  209. package/src/router/router-interfaces.ts +25 -2
  210. package/src/router/router-options.ts +8 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -12
  212. package/src/router/segment-resolution/loader-cache.ts +205 -45
  213. package/src/router/segment-resolution/loader-mask.ts +12 -22
  214. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  215. package/src/router/segment-resolution/mask-nested.ts +7 -7
  216. package/src/router/segment-resolution/revalidation.ts +34 -6
  217. package/src/router/segment-wrappers.ts +4 -0
  218. package/src/rsc/render-pipeline.ts +3 -0
  219. package/src/rsc/response-route-handler.ts +11 -3
  220. package/src/rsc/rsc-rendering.ts +8 -45
  221. package/src/rsc/shell-capture-constants.ts +13 -0
  222. package/src/rsc/shell-capture.ts +111 -90
  223. package/src/rsc/ssr-setup.ts +14 -8
  224. package/src/rsc/types.ts +11 -0
  225. package/src/server/context.ts +88 -11
  226. package/src/server/cookie-store.ts +5 -4
  227. package/src/server/handle-store.ts +232 -7
  228. package/src/server/request-context.ts +35 -30
  229. package/src/ssr/index.tsx +40 -7
  230. package/src/ssr-suspension-warning.ts +2 -13
  231. package/src/static-handler.ts +4 -1
  232. package/src/testing/flight-runtime.d.ts +21 -0
  233. package/src/testing/generated-routes.ts +2 -2
  234. package/src/testing/internal/context.ts +1 -1
  235. package/src/testing/internal/flight-client-globals.ts +7 -5
  236. package/src/testing/render-route.tsx +85 -24
  237. package/src/testing/run-loader.ts +5 -1
  238. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  239. package/src/testing/vitest.ts +62 -1
  240. package/src/theme/ThemeProvider.tsx +14 -12
  241. package/src/theme/ThemeScript.tsx +3 -3
  242. package/src/thenable-status.ts +16 -0
  243. package/src/types/cache-types.ts +14 -10
  244. package/src/types/handler-context.ts +14 -0
  245. package/src/types/loader-types.ts +8 -6
  246. package/src/urls/response-types.ts +3 -23
  247. package/src/use-loader.tsx +24 -4
  248. package/src/vite/plugins/use-cache-transform.ts +118 -74
  249. package/src/vite/rango.ts +7 -3
@@ -40,12 +40,16 @@ re-renders on every handle update, late loader pushes included.
40
40
  Handles can be passed as props from server to client components:
41
41
 
42
42
  ```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
- });
43
+ // Server component (route handler)
44
+ path(
45
+ "/dashboard",
46
+ (ctx) => {
47
+ const push = ctx.use(Breadcrumbs);
48
+ push({ label: "Dashboard", href: "/dashboard" });
49
+ return <DashboardNav handle={Breadcrumbs} />;
50
+ },
51
+ { name: "dashboard" },
52
+ );
49
53
  ```
50
54
 
51
55
  ```tsx
@@ -58,7 +62,9 @@ function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
58
62
  return (
59
63
  <nav>
60
64
  {crumbs.map((c) => (
61
- <a href={c.href}>{c.label}</a>
65
+ <a key={c.href} href={c.href}>
66
+ {c.label}
67
+ </a>
62
68
  ))}
63
69
  </nav>
64
70
  );
@@ -78,33 +84,42 @@ tracking actions called outside a `<form action={...}>` flow.
78
84
 
79
85
  ### useAction()
80
86
 
81
- Track state of server action invocations:
87
+ Track the lifecycle of a server action, wherever on the page it was invoked.
88
+ If the same action fires several times, the hook reports the latest call.
82
89
 
83
90
  ```tsx
84
91
  "use client";
85
92
  import { useAction } from "@rangojs/router/client";
86
- import { addToCart } from "../actions/cart";
93
+ import { addToCart } from "../actions/cart"; // addToCart(productId: string)
87
94
 
88
95
  function AddToCartButton({ productId }: { productId: string }) {
89
96
  const { state, error, result } = useAction(addToCart);
90
97
 
91
98
  // state: 'idle' | 'loading' | 'streaming'
92
99
  // actionId: string | null
93
- // payload: unknown | null (input data)
94
- // error: Error | null
100
+ // payload: unknown[] | FormData | null (the call's arguments)
101
+ // error: unknown | null (whatever the action threw)
95
102
  // result: unknown | null (return value)
96
103
 
97
104
  return (
98
- <form action={addToCart}>
99
- <input type="hidden" name="productId" value={productId} />
105
+ // .bind keeps the form progressively enhanced; useAction still matches it
106
+ <form action={addToCart.bind(null, productId)}>
100
107
  <button disabled={state === "loading"}>
101
108
  {state === "loading" ? "Adding..." : "Add to Cart"}
102
109
  </button>
103
- {error && <p className="error">{error.message}</p>}
110
+ {error instanceof Error && <p className="error">{error.message}</p>}
104
111
  </form>
105
112
  );
106
113
  }
107
114
 
108
- // Match by string suffix (convenient but may be ambiguous)
109
- const isLoading = useAction("addToCart", (s) => s.state === "loading");
115
+ // With a selector (re-renders only when the selected value changes)
116
+ const isLoading = useAction(addToCart, (s) => s.state === "loading");
117
+
118
+ // Match by export name (suffix match on "#addToCart"; ambiguous if two
119
+ // "use server" files export the same name)
120
+ const isAdding = useAction("addToCart", (s) => s.state === "loading");
110
121
  ```
122
+
123
+ Pass an action imported directly into the client component, or its export
124
+ name as a string. An action received as a prop from a server component has
125
+ 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,21 @@ 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.
218
+
219
+ Server actions already invalidate automatically; to suppress that for a no-op
220
+ action, call `keepClientCache()` inside it (see `/server-actions` → "Client
221
+ Cache After an Action").
200
222
 
201
223
  ```tsx
202
224
  "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