@rangojs/router 0.16.0 → 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 (245) 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 +7 -4
  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 +33 -9
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +1 -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/types/cache-types.d.ts +14 -10
  67. package/dist/types/types/handler-context.d.ts +14 -0
  68. package/dist/types/types/loader-types.d.ts +8 -6
  69. package/dist/types/urls/response-types.d.ts +3 -12
  70. package/dist/types/use-loader.d.ts +12 -3
  71. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  72. package/dist/types/vite/rango.d.ts +1 -1
  73. package/dist/vite/index.js +1381 -1346
  74. package/package.json +2 -2
  75. package/skills/api-client/SKILL.md +22 -10
  76. package/skills/breadcrumbs/SKILL.md +65 -22
  77. package/skills/bundle-analysis/SKILL.md +22 -22
  78. package/skills/cache-guide/SKILL.md +207 -97
  79. package/skills/caching/SKILL.md +295 -93
  80. package/skills/catalog.json +16 -16
  81. package/skills/client-urls/SKILL.md +53 -31
  82. package/skills/cloudflare/SKILL.md +72 -20
  83. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  84. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  85. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  86. package/skills/comparison/SKILL.md +5 -0
  87. package/skills/comparison/references/framework-comparison.md +39 -33
  88. package/skills/composability/SKILL.md +38 -29
  89. package/skills/css/SKILL.md +10 -13
  90. package/skills/debug-manifest/SKILL.md +74 -31
  91. package/skills/defer-hydration/SKILL.md +2 -1
  92. package/skills/deployment-caching/SKILL.md +6 -1
  93. package/skills/document-cache/SKILL.md +92 -28
  94. package/skills/fonts/SKILL.md +16 -8
  95. package/skills/handler-use/SKILL.md +53 -17
  96. package/skills/hooks/SKILL.md +28 -20
  97. package/skills/hooks/data.md +23 -3
  98. package/skills/hooks/handle-and-actions.md +31 -16
  99. package/skills/hooks/navigation.md +39 -10
  100. package/skills/hooks/outlets.md +13 -6
  101. package/skills/hooks/state.md +37 -15
  102. package/skills/hooks/urls.md +7 -5
  103. package/skills/host-router/SKILL.md +64 -12
  104. package/skills/i18n/SKILL.md +60 -34
  105. package/skills/intercept/SKILL.md +161 -26
  106. package/skills/layout/SKILL.md +106 -50
  107. package/skills/links/SKILL.md +51 -19
  108. package/skills/loader/SKILL.md +242 -106
  109. package/skills/middleware/SKILL.md +119 -23
  110. package/skills/migrate-nextjs/SKILL.md +100 -45
  111. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  112. package/skills/migrate-react-router/SKILL.md +49 -38
  113. package/skills/migrate-react-router/component-migration.md +25 -6
  114. package/skills/migrate-react-router/data-and-actions.md +18 -5
  115. package/skills/migrate-react-router/route-mapping.md +12 -6
  116. package/skills/mime-routes/SKILL.md +12 -7
  117. package/skills/observability/SKILL.md +28 -14
  118. package/skills/parallel/SKILL.md +75 -39
  119. package/skills/ppr/SKILL.md +345 -150
  120. package/skills/prerender/SKILL.md +189 -100
  121. package/skills/rango/SKILL.md +65 -34
  122. package/skills/react-compiler/SKILL.md +6 -5
  123. package/skills/response-routes/SKILL.md +133 -25
  124. package/skills/route/SKILL.md +165 -60
  125. package/skills/router-setup/SKILL.md +418 -240
  126. package/skills/scripts/SKILL.md +31 -20
  127. package/skills/server-actions/SKILL.md +220 -59
  128. package/skills/shell-manifest/SKILL.md +12 -7
  129. package/skills/streams-and-websockets/SKILL.md +32 -14
  130. package/skills/tailwind/SKILL.md +37 -33
  131. package/skills/testing/SKILL.md +68 -32
  132. package/skills/testing/cache-prerender.md +55 -29
  133. package/skills/testing/client-components.md +59 -6
  134. package/skills/testing/e2e-parity.md +12 -2
  135. package/skills/testing/flight.md +19 -11
  136. package/skills/testing/handles.md +8 -5
  137. package/skills/testing/loader.md +8 -5
  138. package/skills/testing/middleware.md +8 -3
  139. package/skills/testing/predicates.md +120 -0
  140. package/skills/testing/render-handler.md +70 -22
  141. package/skills/testing/response-routes.md +11 -4
  142. package/skills/testing/reverse-and-types.md +20 -14
  143. package/skills/testing/server-actions.md +12 -3
  144. package/skills/testing/server-tree.md +9 -0
  145. package/skills/testing/setup.md +9 -5
  146. package/skills/theme/SKILL.md +106 -29
  147. package/skills/typesafety/SKILL.md +20 -9
  148. package/skills/typesafety/env-and-bindings.md +126 -47
  149. package/skills/typesafety/generated-files-and-cli.md +26 -14
  150. package/skills/typesafety/params-and-search.md +44 -19
  151. package/skills/typesafety/route-types.md +50 -14
  152. package/skills/use-cache/SKILL.md +200 -82
  153. package/skills/vercel/SKILL.md +21 -11
  154. package/skills/view-transitions/SKILL.md +41 -20
  155. package/src/__augment-tests__/augmented.check.ts +26 -1
  156. package/src/browser/index.ts +2 -2
  157. package/src/browser/navigation-bridge.ts +3 -4
  158. package/src/browser/navigation-transaction.ts +22 -2
  159. package/src/browser/partial-update.ts +35 -25
  160. package/src/browser/react/ScrollRestoration.tsx +6 -6
  161. package/src/browser/rsc-router.tsx +1 -1
  162. package/src/browser/scroll-restoration.ts +53 -6
  163. package/src/cache/cache-exec-scope.ts +37 -4
  164. package/src/cache/cache-key-utils.ts +1 -6
  165. package/src/cache/cache-runtime.ts +230 -61
  166. package/src/cache/cache-scope.ts +76 -12
  167. package/src/cache/document-cache.ts +15 -0
  168. package/src/cache/handle-capture.ts +90 -10
  169. package/src/cache/handle-snapshot.ts +132 -28
  170. package/src/cache/segment-codec.ts +26 -7
  171. package/src/cache/tag-invalidation.ts +4 -3
  172. package/src/cache/taint.ts +1 -1
  173. package/src/cache/types.ts +16 -3
  174. package/src/client.rsc.tsx +10 -9
  175. package/src/client.tsx +5 -4
  176. package/src/components/DefaultDocument.tsx +7 -2
  177. package/src/context-var.ts +6 -5
  178. package/src/escape-script.ts +1 -1
  179. package/src/handles/MetaTags.tsx +13 -11
  180. package/src/handles/Scripts.tsx +11 -10
  181. package/src/handles/meta.ts +2 -2
  182. package/src/handles/script.ts +17 -16
  183. package/src/href-client.ts +1 -1
  184. package/src/html.ts +12 -0
  185. package/src/index.rsc.ts +5 -0
  186. package/src/index.ts +1 -0
  187. package/src/loader-store.ts +7 -4
  188. package/src/prerender/build-shell-capture.ts +8 -11
  189. package/src/reverse.ts +44 -4
  190. package/src/route-definition/dsl-helpers.ts +117 -30
  191. package/src/route-definition/helpers-types.ts +33 -9
  192. package/src/route-definition/resolve-handler-use.ts +121 -6
  193. package/src/route-types.ts +0 -3
  194. package/src/router/debug-manifest.ts +10 -2
  195. package/src/router/error-handling.ts +51 -15
  196. package/src/router/handler-context.ts +34 -25
  197. package/src/router/intercept-resolution.ts +125 -39
  198. package/src/router/loader-resolution.ts +56 -11
  199. package/src/router/match-api.ts +12 -17
  200. package/src/router/match-middleware/background-revalidation.ts +161 -96
  201. package/src/router/match-middleware/cache-lookup.ts +93 -10
  202. package/src/router/match-middleware/cache-store.ts +17 -90
  203. package/src/router/middleware-types.ts +3 -5
  204. package/src/router/middleware.ts +52 -9
  205. package/src/router/prerender-match.ts +53 -19
  206. package/src/router/route-snapshot.ts +11 -1
  207. package/src/router/router-context.ts +2 -0
  208. package/src/router/router-interfaces.ts +25 -2
  209. package/src/router/router-options.ts +8 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -12
  211. package/src/router/segment-resolution/loader-cache.ts +205 -45
  212. package/src/router/segment-resolution/loader-mask.ts +12 -22
  213. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  214. package/src/router/segment-resolution/mask-nested.ts +7 -7
  215. package/src/router/segment-resolution/revalidation.ts +34 -6
  216. package/src/router/segment-wrappers.ts +4 -0
  217. package/src/rsc/render-pipeline.ts +3 -0
  218. package/src/rsc/response-route-handler.ts +11 -3
  219. package/src/rsc/rsc-rendering.ts +8 -45
  220. package/src/rsc/shell-capture-constants.ts +13 -0
  221. package/src/rsc/shell-capture.ts +111 -90
  222. package/src/rsc/ssr-setup.ts +14 -8
  223. package/src/rsc/types.ts +11 -0
  224. package/src/server/context.ts +88 -11
  225. package/src/server/cookie-store.ts +5 -4
  226. package/src/server/handle-store.ts +232 -7
  227. package/src/server/request-context.ts +35 -30
  228. package/src/ssr/index.tsx +40 -7
  229. package/src/static-handler.ts +4 -1
  230. package/src/testing/flight-runtime.d.ts +21 -0
  231. package/src/testing/generated-routes.ts +2 -2
  232. package/src/testing/internal/context.ts +1 -1
  233. package/src/testing/internal/flight-client-globals.ts +7 -5
  234. package/src/testing/render-route.tsx +85 -24
  235. package/src/testing/run-loader.ts +5 -1
  236. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  237. package/src/testing/vitest.ts +62 -1
  238. package/src/theme/ThemeScript.tsx +3 -3
  239. package/src/types/cache-types.ts +14 -10
  240. package/src/types/handler-context.ts +14 -0
  241. package/src/types/loader-types.ts +8 -6
  242. package/src/urls/response-types.ts +3 -23
  243. package/src/use-loader.tsx +12 -3
  244. package/src/vite/plugins/use-cache-transform.ts +118 -74
  245. package/src/vite/rango.ts +7 -3
@@ -1,11 +1,19 @@
1
1
  ---
2
2
  name: route
3
- description: Define routes with path() in @rangojs/router. Use when creating a new page or route, or asking how to define a URL path and its handler.
3
+ description: Define routes with path() in @rangojs/router — URL params, catch-alls, typed search params, the handler context, handler-to-child data, and redirects. Use when creating a new page or route, or asking how to define a URL path and its handler.
4
4
  argument-hint: [pattern]
5
5
  ---
6
6
 
7
7
  # Defining Routes with path()
8
8
 
9
+ `path(pattern, handler, options?, use?)` maps a URL pattern to a handler inside
10
+ `urls()`. This skill covers patterns and params, typed search params, the handler
11
+ context, passing handler data to child segments, and redirects.
12
+
13
+ Related skills: `/layout` (wrap routes in shared UI), `/response-routes`
14
+ (`path.json()`, `path.text()`, … endpoints), `/links` (URL generation),
15
+ `/composability` (`include()` and reusable config), `/loader` (data).
16
+
9
17
  ## Basic Route
10
18
 
11
19
  ```typescript
@@ -57,7 +65,8 @@ see `/i18n`.
57
65
 
58
66
  A catch-all consumes the **rest of the path** and exposes it as a single
59
67
  decoded string at `ctx.params.<name>`, with the internal `/` separators kept.
60
- It must be the **last** segment of the pattern.
68
+ It only acts as a catch-all in the **last** segment of the pattern (see the
69
+ parsing rule at the end of this section).
61
70
 
62
71
  - `:name+` — **one-or-more** segments (Next `[...name]`, React-Router splat).
63
72
  `/docs/:slug+` matches `/docs/a` and `/docs/a/b/c`, but **not** the bare
@@ -81,9 +90,9 @@ urls(({ path }) => [
81
90
  ```
82
91
 
83
92
  `ctx.params.<name>` is always a `string` for a catch-all (never `undefined`) —
84
- `:name*` binds `""` for the empty case, so read it directly. `reverse()` /
85
- `ctx.reverse()` rebuild the URL with separators preserved:
86
- `reverse("docs", { slug: "a/b" })` -> `/docs/a/b`.
93
+ `:name*` binds `""` for the empty case, so read it directly. URL generation
94
+ (`ctx.reverse()`, `href()`) rebuilds the URL with separators preserved:
95
+ `ctx.reverse("docs", { slug: "a/b" })` -> `/docs/a/b`.
87
96
 
88
97
  The value is the URL-decoded remainder. `split("/")` recovers the segments in the
89
98
  common case, but note that a segment containing an encoded slash (`%2F`) decodes
@@ -93,8 +102,27 @@ raw pathname instead.
93
102
 
94
103
  The bare unnamed wildcard `path("/files/*", …)` still works and is read at
95
104
  `ctx.params["*"]`; prefer a named catch-all when you want a typed param key.
96
- Combining a modifier with `?`, a literal suffix, or a constraint
97
- (`:slug*?`, `:slug*.html`, `:slug(a|b)+`) is rejected at build time.
105
+
106
+ Parsing rule: `+` / `*` is a catch-all modifier **only** when it is the bare
107
+ final character of the last segment. Anywhere else it is read as a literal
108
+ suffix character, not rejected: `/docs/:slug+/edit` matches `/docs/x+/edit`,
109
+ and `:slug*.html`, `:slug?*`, and `:slug(a|b)+` are ordinary params followed by
110
+ a literal suffix.
111
+
112
+ ### Constrained and suffixed params
113
+
114
+ ```typescript
115
+ urls(({ path }) => [
116
+ // Only /en/about or /gb/about match; ctx.params.locale is typed "en" | "gb"
117
+ path("/:locale(en|gb)/about", AboutPage, { name: "about" }),
118
+
119
+ // Constrained + optional: /pricing, /en/pricing, /gb/pricing
120
+ path("/:locale(en|gb)?/pricing", PricingPage, { name: "pricing" }),
121
+
122
+ // Literal suffix: /files/report.pdf -> ctx.params.name === "report"
123
+ path("/files/:name.pdf", PdfViewer, { name: "pdf" }),
124
+ ]);
125
+ ```
98
126
 
99
127
  ## Route Handler Patterns
100
128
 
@@ -135,12 +163,25 @@ path("/product/:slug", async (ctx) => {
135
163
 
136
164
  ## Route Options
137
165
 
166
+ All options are optional; with none, pass the `use` callback as the third
167
+ argument: `path("/x", Page, () => [loader(L)])`.
168
+
138
169
  ```typescript
139
170
  path("/product/:slug", ProductPage, {
140
- name: "product", // Route name for href() and navigation
171
+ name: "product", // route name for href(), ctx.reverse(), Handler<"product">
172
+ search: { tab: "string?" }, // typed ctx.search (next section)
173
+ trailingSlash: "never", // "never" | "always" | "ignore"
174
+ ppr: true, // serve a cached HTML shell with live holes (see /ppr)
141
175
  });
142
176
  ```
143
177
 
178
+ - `name` — unnamed routes still match but can't be reversed by name or typed
179
+ with `Handler<"name">`. Inside an `include()`, the include's `name` option
180
+ decides whether the name is visible globally (see "Nested Routes" below).
181
+ - `trailingSlash` — `"never"` redirects `/docs/` to `/docs`, `"always"` redirects `/docs`
182
+ to `/docs/`, `"ignore"` matches both. Unset, the pattern's own trailing slash
183
+ decides.
184
+
144
185
  ### Typed Search Params
145
186
 
146
187
  Add a `search` schema to get typed `ctx.search`:
@@ -158,24 +199,31 @@ Use `Handler<"name">` for typed search params (resolves from the generated route
158
199
  import type { Handler } from "@rangojs/router";
159
200
 
160
201
  export const SearchPage: Handler<"search"> = (ctx) => {
161
- // ctx.search is typed: { q: string; page?: number; sort?: string }
162
- const { q, page, sort } = ctx.search;
202
+ // ctx.search is typed: { q: string | undefined; page?: number; sort?: string }
203
+ const { q = "", page = 1, sort } = ctx.search;
163
204
  // ctx.searchParams is always URLSearchParams
164
205
  return <SearchResults q={q} page={page} sort={sort} />;
165
206
  };
166
207
  ```
167
208
 
168
209
  Supported types: `"string"`, `"number"`, `"boolean"`, with `?` suffix for optional.
169
- Missing params are `undefined` regardless of required/optional. The required/optional
170
- distinction is a consumer-facing contract (for `href()` and `reverse()` autocomplete).
210
+
211
+ - **Missing params are `undefined` regardless of required/optional** — a client
212
+ can always omit them, so the handler must default or check. The
213
+ required/optional distinction is a consumer-facing contract that drives
214
+ `href()` / `ctx.reverse()` autocomplete.
215
+ - `"number"` accepts decimal numerals (`42`, `-3.5`, `1e3`). Empty values, hex
216
+ (`0x10`), and non-finite values are treated as missing, not coerced.
217
+ - `"boolean"` is `true` for `"true"` / `"1"` and `false` for any other present
218
+ value.
171
219
 
172
220
  Use `RouteSearchParams<"name">` and `RouteParams<"name">` to extract types for props:
173
221
 
174
222
  ```typescript
175
223
  import type { RouteSearchParams, RouteParams } from "@rangojs/router";
176
224
 
177
- type SP = RouteSearchParams<"search">; // { q: string; page?: number; sort?: string }
178
- type P = RouteParams<"blogPost">; // { slug: string }
225
+ type SP = RouteSearchParams<"search">; // { q: string | undefined; page?: number; sort?: string }
226
+ type P = RouteParams<"blogPost">; // { year: string; month: string; slug: string }
179
227
  ```
180
228
 
181
229
  ## Route Children
@@ -194,15 +242,13 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
194
242
 
195
243
  When a route has children (orphan layouts, parallels), the handler executes
196
244
  first. Use `ctx.set(key, value)` to share data with children, who read it
197
- via `ctx.get(key)`. Caching wraps all segments together, so either all run
198
- or none do.
245
+ via `ctx.get(key)`. A route-level `cache()` wraps all of the entry's segments
246
+ together, so on a cache hit none of them run and on a miss all of them do.
199
247
 
200
- This pattern is also safe under partial action revalidation: on an action,
201
- the route entry re-runs as a unit by default — route segment, loaders, and
202
- `belongsToRoute` children (orphan layouts, entry parallels) all seed
203
- revalidate-true, with handler-first ordering preserved. Handler-set data
204
- stays consistent with no configuration. See `/rango` → "Passing data down
205
- the tree" for the safest-first ladder.
248
+ This stays consistent after actions with no configuration: on an action, the
249
+ route segment, its loaders, and the children declared inside the `path()`
250
+ (orphan layouts and their parallels) all re-run together, handler first. See
251
+ `/rango` → "Passing data down the tree" for the safest-first ladder.
206
252
 
207
253
  ### Typed context variables with createVar
208
254
 
@@ -248,8 +294,10 @@ path("/dashboard/:id", async (ctx) => {
248
294
  String keys still work (`ctx.set("key", value)` / `ctx.get("key")`), but
249
295
  `createVar<T>()` is preferred for type safety.
250
296
 
251
- Only route handlers and middleware can call `ctx.set()`. Layouts, parallels,
252
- and intercepts can only read via `ctx.get()`.
297
+ Write with `ctx.set()` from middleware, route handlers, and layout handlers;
298
+ read with `ctx.get()` in the segments they wrap. Handlers run before their
299
+ orphan layouts and parallels (handler-first), so those children see the value.
300
+ Parallels and intercepts render last, so treat them as readers.
253
301
 
254
302
  #### Non-cacheable context variables
255
303
 
@@ -268,19 +316,25 @@ ctx.set(Theme, userTheme, { cache: false });
268
316
  "Least cacheable wins" — if either the var definition or the write site says
269
317
  `cache: false`, the value is non-cacheable.
270
318
 
271
- Reading a non-cacheable var inside `cache()` or `"use cache"` throws at
272
- runtime. This prevents request-specific data from leaking into cached output:
319
+ Reading a non-cacheable var directly with `ctx.get()` inside a `cache()`
320
+ boundary throws at runtime, so request-specific data can't be baked into a
321
+ cached segment:
273
322
 
274
323
  ```typescript
275
- // This throws — Session is non-cacheable
276
- async function CachedWidget(ctx) {
277
- "use cache";
278
- const session = ctx.get(Session); // Error: non-cacheable var read inside cache scope
279
- return <Widget />;
280
- }
324
+ cache({ ttl: 60 }, () => [
325
+ path("/account", (ctx) => {
326
+ const session = ctx.get(Session); // throws: non-cacheable read inside cache()
327
+ return <Account session={session} />;
328
+ }, { name: "account" }),
329
+ ]);
281
330
  ```
282
331
 
283
- Cacheable vars (the default) can be read freely inside cache scopes.
332
+ The same read throws inside a `"use cache"` function body, where the value
333
+ would be stored without being part of the key — pass it in as an argument
334
+ instead. The guard does not follow derived values (a string copied out of
335
+ `Session` and cached elsewhere is not tracked). Loaders are exempt — they run
336
+ fresh on every request — but a `"use cache"` function a loader calls is not. Cacheable vars (the default) can be read freely
337
+ inside cache scopes. See `/cache-guide` → "Context Variable Cache Safety".
284
338
 
285
339
  ### Revalidation Contracts for Handler Data
286
340
 
@@ -295,7 +349,8 @@ the route handler and its children re-run together by default, so handler
295
349
  data stays consistent on its own. Contracts matter in two cases:
296
350
 
297
351
  1. **You narrow the entry's revalidation** with a predicate that can return a
298
- hard `false` (e.g. bare `ctx.isAction(X)`). A hard `false` on one side of a
352
+ hard `false` (e.g. `ctx.isAction() ? ctx.isAction(X) : undefined`). A hard
353
+ `false` on one side of a
299
354
  producer/consumer pair desyncs it — the child re-runs by default and reads
300
355
  `undefined`, or vice versa. Put the same named contract on the route and
301
356
  its dependent children so they narrow together.
@@ -305,17 +360,20 @@ data stays consistent on its own. Contracts matter in two cases:
305
360
 
306
361
  ```typescript
307
362
  // revalidation-contracts.ts
363
+ import type { Revalidate } from "@rangojs/router";
308
364
  import * as CheckoutActions from "./actions/checkout";
309
365
 
310
- // Defer (|| undefined), not ?? false: a hard `false` short-circuits the chain,
311
- // so when the same segment composes multiple contracts the later ones never run.
312
- export const revalidateCheckoutData = (ctx) =>
313
- ctx.isAction(CheckoutActions) || undefined;
366
+ // The route and its children re-run after every action by default, so this
367
+ // contract narrows them together: after an action, re-run only for checkout
368
+ // actions; on navigation, undefined keeps the default. After an action it is a
369
+ // hard decision, so later revalidators on the same segment do not run.
370
+ export const revalidateCheckoutData: Revalidate = (ctx) =>
371
+ ctx.isAction() ? ctx.isAction(CheckoutActions) : undefined;
314
372
 
315
373
  path("/checkout", CheckoutPage, { name: "checkout" }, () => [
316
- revalidate(revalidateCheckoutData), // producer (route handler) reruns
374
+ revalidate(revalidateCheckoutData), // producer (route handler)
317
375
  layout(CheckoutLayout, () => [
318
- revalidate(revalidateCheckoutData), // consumer reruns
376
+ revalidate(revalidateCheckoutData), // consumer
319
377
  parallel({ "@summary": CheckoutSummary }, () => [
320
378
  revalidate(revalidateCheckoutData),
321
379
  ]),
@@ -323,8 +381,10 @@ path("/checkout", CheckoutPage, { name: "checkout" }, () => [
323
381
  ]);
324
382
  ```
325
383
 
326
- If children depend on multiple upstream domains, compose multiple contracts on
327
- the same segment (`revalidateAuthData`, `revalidateCheckoutData`, and so on).
384
+ If children depend on multiple upstream domains, match them in one narrowing
385
+ contract (`ctx.isAction(CheckoutActions, AuthActions)`): a second narrowing
386
+ contract on the same segment would never run, because the first one's hard
387
+ `false` ends the chain.
328
388
 
329
389
  For cleaner route trees, expose contract helpers and spread them:
330
390
 
@@ -341,6 +401,9 @@ path("/checkout", CheckoutPage, { name: "checkout" }, () => [
341
401
 
342
402
  ## Redirects
343
403
 
404
+ `redirect(url, statusOrOptions?)` returns a `Response` (default status `302`).
405
+ Return it from a handler or middleware, or throw it from a loader.
406
+
344
407
  ### Basic redirect
345
408
 
346
409
  ```typescript
@@ -355,6 +418,22 @@ path("/old-page", () => redirect("/new-page"), { name: "oldPage" });
355
418
  path("/moved", () => redirect("/new-location", 301), { name: "moved" });
356
419
  ```
357
420
 
421
+ Root-relative targets are prefixed with the router's `basename` automatically
422
+ (an already-prefixed URL is left alone).
423
+
424
+ ### Off-origin redirects
425
+
426
+ Cross-origin targets are blocked by a same-origin guard and replaced with the
427
+ app root. Opt in explicitly for an intended off-host redirect:
428
+
429
+ ```typescript
430
+ path(
431
+ "/login",
432
+ () => redirect("https://accounts.example.com/oauth", { external: true }),
433
+ { name: "login" },
434
+ );
435
+ ```
436
+
358
437
  > **Redirecting from a route with `loading()`:** an `async` handler that returns
359
438
  > a `Response`/`redirect()` on a route that also declares `loading()` is streamed,
360
439
  > so the redirect is rendered into the RSC stream instead of becoming an HTTP
@@ -399,7 +478,8 @@ path(
399
478
  );
400
479
  ```
401
480
 
402
- Read the state on the target page with `useLocationState(FlashMessage)`. The
481
+ `state` takes one entry or an array. Read it on the target page with
482
+ `useLocationState(FlashMessage)` (from `@rangojs/router/client`). The
403
483
  `{ flash: true }` option makes it auto-clear. Without `{ flash: true }`,
404
484
  state persists on back/forward. See `/hooks` for details.
405
485
 
@@ -426,24 +506,44 @@ State flows to the browser via the RSC payload and is merged into
426
506
  Every handler receives a context object:
427
507
 
428
508
  ```typescript
509
+ // Simplified sketch. Import the real type with
510
+ // `import type { HandlerContext } from "@rangojs/router"`.
429
511
  interface HandlerContext<TParams = {}, TEnv = DefaultEnv, TSearch = {}> {
430
- params: TParams; // URL parameters
431
- request: Request; // Original request
432
- searchParams: URLSearchParams; // Query params (always URLSearchParams)
433
- search: {} | ResolveSearchSchema<TSearch>; // Typed search params (from search schema)
434
- url: URL; // Parsed URL
435
- env: TEnv; // Environment (bindings + variables)
436
- set(key: string, value: any): void; // Set context variable (untyped string key)
437
- set<T>(contextVar: ContextVar<T>, value: T): void; // Set typed context variable
438
- get(key: string): any; // Read context variable (untyped string key)
439
- get<T>(contextVar: ContextVar<T>): T | undefined; // Read typed context variable
440
- use<T>(handle: Handle<T>): T; // Access handles
512
+ // Request
513
+ params: TParams; // URL params
514
+ request: Request; // original request (raw transport URL, headers, body)
515
+ url: URL; // request URL with internal _rsc* params stripped
516
+ pathname: string;
517
+ searchParams: URLSearchParams; // always URLSearchParams (_rsc* stripped)
518
+ search: ResolveSearchSchema<TSearch>; // typed from the search schema ({} without one)
519
+ env: TEnv; // platform bindings
520
+ routeName?: string; // matched route name (undefined for unnamed routes)
521
+
522
+ // Data flow
523
+ get<T>(contextVar: ContextVar<T>): T | undefined; // also accepts a string key
524
+ set<T>(
525
+ contextVar: ContextVar<T>,
526
+ value: T,
527
+ options?: { cache?: boolean },
528
+ ): void;
529
+ use<T>(loader: LoaderDefinition<T>): Promise<T>; // loader data (memoized per request)
530
+ use<T>(handle: Handle<T>): HandlePush<T>; // push function for a handle
531
+
532
+ // Response
533
+ headers: Headers; // response headers, merged into the final response
534
+ setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
441
535
  reverse(
442
536
  name: string,
443
537
  params?: Record<string, string>,
444
538
  search?: Record<string, unknown>,
445
- ): string; // URL generation
446
- setLocationState(entries: LocationStateEntry[]): void; // Attach state to response
539
+ ): string;
540
+ waitUntil(fn: () => Promise<void>): void; // work that continues after the response
541
+
542
+ // Mode
543
+ build: boolean; // true while pre-rendering at build time
544
+ dev: boolean; // true under Vite dev
545
+ dynamic(): void; // opt this request out of PPR shell capture (see /ppr)
546
+ theme?: Theme; // plus setTheme?() — only when the router enables themes (/theme)
447
547
  }
448
548
  ```
449
549
 
@@ -460,9 +560,12 @@ path("/product/:slug", (ctx) => {
460
560
  // Access platform bindings
461
561
  const db = ctx.env.DB;
462
562
 
463
- // Access handles
464
- const breadcrumbs = ctx.use(Breadcrumbs);
465
- breadcrumbs.push({ label: "Product", href: `/product/${slug}` });
563
+ // Push handle data: ctx.use(Handle) returns a push function
564
+ const pushCrumb = ctx.use(Breadcrumbs);
565
+ pushCrumb({ label: "Product", href: `/product/${slug}` });
566
+
567
+ // Set a response header
568
+ ctx.headers.set("Cache-Control", "private, max-age=60");
466
569
 
467
570
  return <ProductPage slug={slug} tab={tab} />;
468
571
  }, { name: "product" })
@@ -505,6 +608,8 @@ A route can configure its own `transition()` — the wrap goes around the route'
505
608
  Page handlers can carry their own loader, middleware, error boundaries, parallels, and other defaults via a `.use` callback — so the page is self-contained and reusable across mount sites without re-wiring the same items.
506
609
 
507
610
  ```typescript
611
+ import { loader, loading, middleware, type Handler } from "@rangojs/router";
612
+
508
613
  const ProductPage: Handler<"/product/:slug"> = async (ctx) => {
509
614
  const product = await ctx.use(ProductLoader);
510
615
  return <ProductView product={product} />;
@@ -514,7 +619,7 @@ ProductPage.use = () => [
514
619
  loading(<ProductSkeleton />),
515
620
  middleware(async (ctx, next) => {
516
621
  await next();
517
- ctx.header("Cache-Control", "private, max-age=60");
622
+ ctx.headers.set("Cache-Control", "private, max-age=60");
518
623
  }),
519
624
  ];
520
625