@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
@@ -22,6 +22,7 @@
22
22
  | `cacheStore` | `SegmentCacheStore` | Backing store for `use cache` functions (same shape as `createRouter({ cache })`). Without it, cached functions run uncached and their guards never fire. |
23
23
  | `cacheProfiles` | `Record<string, CacheProfile>` | Profiles for `use cache: "name"`, same shape as `createRouter({ cacheProfiles })`. An unknown profile throws. |
24
24
  | `theme` | `ThemeConfig \| true` | Theme config (same shape as `createRouter({ theme })`). Without it `ctx.theme` / `ctx.setTheme` are inert. |
25
+ | `build` | `boolean` | Seed `ctx.build` (default `false`) for code that branches on the build-time PPR shell-capture pass. With `true`, `ctx.waitUntil()` is inert, as at build time. |
25
26
  | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie an action calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production; override `prefix`/`routerId` to match `createRouter({ stateCookiePrefix, id })`, or `version` (value is `{version}:{timestamp}`, default `"0"`). |
26
27
 
27
28
  ### Context — `RequestContext<TEnv>` (what your code receives)
@@ -52,7 +53,12 @@
52
53
  | `stateCookieName` | `string` | The resolved rango state cookie name this run seeded (default `rango-state_router_0`). Assert an `invalidateClientCache()` rotation against it without recomputing. |
53
54
  | `locationState` | `Record<string, unknown>` | The flash set via `ctx.setLocationState()` / `redirect({ state })`, as the flat `{ key: value }` the client reads. |
54
55
 
55
- Low-level variant: when you already hold a context from `createTestRequestContext(opts)`, call `runWithRequestContext(ctx, fn)` (re-exported from `@rangojs/router/testing`) to enter it directly. `runInRequestContext` is the one-call convenience over `createTestRequestContext` + `runWithRequestContext`.
56
+ Low-level building blocks (all from `@rangojs/router/testing`):
57
+
58
+ - `createTestRequestContext(opts)` builds the context WITHOUT entering it and returns `{ ctx, request, url, variables, stateCookieName }`. Its `ctx.reverse` accepts any name from your `routeMap`.
59
+ - `runWithRequestContext(ctx, fn)` enters a context you already hold. `runInRequestContext` is the one-call convenience over these two, plus the result snapshot.
60
+ - `toRequest(request?, init?)` normalizes a `Request | string | undefined` into a `Request` (strings resolve against `http://localhost/`).
61
+ - `seedVariables(variables, vars?)` writes a `VarsInit` (object or `[key, value]` tuples) into a raw variables record, the same way the `vars` option does.
56
62
 
57
63
  ## Recipe
58
64
 
@@ -60,10 +66,13 @@ Low-level variant: when you already hold a context from `createTestRequestContex
60
66
  import { it, expect } from "vitest";
61
67
  import { runInRequestContext } from "@rangojs/router/testing";
62
68
  import { loginAction } from "../src/actions/login"; // sets a session cookie + flash, then throw redirect("/app")
69
+ import { logoutAction, dismissBannerAction } from "../src/actions/session"; // invalidateClientCache() / keepClientCache()
70
+
71
+ const env = { DB: fakeDb }; // your binding double (see ./bindings.md)
63
72
 
64
73
  it("sets the session cookie + flash and redirects", async () => {
65
74
  const { thrown, cookies, locationState } = await runInRequestContext(
66
- () => loginAction(input),
75
+ () => loginAction({ email: "ada@example.com", password: "pw" }),
67
76
  {
68
77
  env,
69
78
  request: new Request("https://app.test/admin", {
@@ -97,7 +106,7 @@ it("asserts the client-cache directives an action issued", async () => {
97
106
 
98
107
  - The snapshot fires whether `fn` RETURNS or THROWS. A `throw redirect("/app")` on the success path is captured on `thrown` (NOT re-thrown), so no try/catch is needed; assert on `thrown` for a throwing action.
99
108
  - There is no cookies / headers option. Seed a request cookie by passing a full `Request` with the `Cookie` header (as in the recipe).
100
- - `runWithRequestContext(ctx, fn)` is the low-level entry when you already hold a context; `runInRequestContext` is the one-call convenience over `createTestRequestContext` + `runWithRequestContext`.
109
+ - An action has no loader context, so `runLoader` is the wrong shape for it even when the action reads cookies or vars; use `runInRequestContext`.
101
110
  - Platform bindings are yours to double via `env` (see `./bindings.md`).
102
111
 
103
112
  ## See also
@@ -18,6 +18,8 @@
18
18
  | `vars` | `VarsInit` | Context variables visible via `ctx.get(...)`, as a prior middleware would have set them. Object form (`{ user }`) or `[key, value]` tuples. |
19
19
  | `clientComponents` | `Record<string, unknown>` | The `"use client"` components reachable from the tree, keyed by the boundary name to register each as a client reference (in place) so it serializes as an `I` row. Omit when `rangoUseClientTransform()` auto-discovers them, or for pure server-only trees. First-wins per worker; already-registered references are left untouched. |
20
20
 
21
+ It also accepts the rest of `RenderToFlightStringOptions` — `routeMap`, `theme`, `cacheStore`, `cacheProfiles` — with the same meaning as in [`./flight.md`](./flight.md).
22
+
21
23
  ### Context — what your code receives
22
24
 
23
25
  A server component rendered here runs under a real request context: `getRequestContext()` resolves, `ctx.params`/`ctx.routeName`/`ctx.env` reflect the options, `ctx.get(MyVar)` reads a seeded `var`, and cookies come off the request. Same seeding as the handler-test primitives — you render an **element** you build (`<Page />`); to run a route **handler** `(ctx) => rsc` use `renderHandler` (see `./render-handler.md`).
@@ -64,6 +66,10 @@ Every SERVER/HOST element a server component produced (`<article>`, `<h2>`), in
64
66
 
65
67
  Concatenates every string/number leaf of a node's subtree in document order — the clean way to assert rendered text, instead of `JSON.stringify(tree).toContain(...)`.
66
68
 
69
+ #### `assertFlightTreeRuntimeAvailable()`
70
+
71
+ Throws a descriptive error when a `@vitejs/plugin-rsc` subpath the round trip relies on (the vendored client deserializer or `registerClientReference`) no longer exports the expected function — the counterpart of `assertFlightRuntimeAvailable()` in `./flight.md`. Call it in a `beforeAll` to fail fast after a plugin-rsc upgrade.
72
+
67
73
  ## Recipe
68
74
 
69
75
  ```tsx
@@ -74,8 +80,11 @@ import {
74
80
  findElements,
75
81
  textContent,
76
82
  } from "@rangojs/router/testing/flight";
83
+ import { flightMatchers } from "@rangojs/router/testing/flight-matchers";
77
84
  import { PriceTag } from "./PriceTag.js"; // a "use client" component (any filename)
78
85
 
86
+ expect.extend(flightMatchers); // for toMatchFlight below
87
+
79
88
  async function ProductPanel({ amount, asOf }: { amount: number; asOf: Date }) {
80
89
  await Promise.resolve();
81
90
  return (
@@ -12,13 +12,15 @@ Real machinery: Vite transpiles `@rangojs/router`'s shipped TS source and resolv
12
12
  | -------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
13
  | `preset` | `"node" \| "cloudflare"` | Deployment preset, matching `rango({ preset })` in the Vite plugin. `"cloudflare"` additionally stubs the `cloudflare:workers` / `cloudflare:email` runtime virtuals a CF route tree imports. A string (not a boolean) so more presets can be added without an API change. Default `"node"`. |
14
14
 
15
- ### Functions
15
+ ### Exports
16
16
 
17
- | Function | Returns | Use |
17
+ | Export | Returns / value | Use |
18
18
  | --------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
19
19
  | `rangoTestConfig(opts?)` | `{ alias, server: { deps: { inline } } }` | Recommended. Spread into the node/DOM project's `test` block. Bundles the resolve aliases AND `server.deps.inline`. |
20
20
  | `rangoTestAliases(opts?)` | `TestAlias[]` (`{ find, replacement }[]`) | Lower-level. The bare `@rangojs/router` -> `index.rsc.ts` alias plus the `:version` / `@vitejs/plugin-rsc/rsc` (`/rsc/server`, `/rsc/client`) stubs (and CF stubs under `preset:"cloudflare"`). Used in the rsc project's `resolve.alias`. |
21
- | `rangoUseClientTransform()` | a Vite plugin (`{ name, transform }`) | Add to the rsc project `plugins`. Applies the `"use client"` transform so `renderServerTree` auto-discovers client islands from the server tree's imports. |
21
+ | `rangoInlineDeps` | `RegExp[]` (`[/@rangojs[/\\]router/]`) | The `server.deps.inline` patterns on their own. Use in the rsc project (`test.server.deps.inline`), or wherever you call `rangoTestAliases` directly instead of `rangoTestConfig`. |
22
+ | `rangoUseClientTransform()` | a Vite plugin (`{ name, transform }`) | Add to the rsc project `plugins`. Applies the `"use client"` transform so `renderServerTree` / `renderHandler` auto-discover client islands from the server tree's imports. |
23
+ | `rangoUseCacheTransform()` | a Vite plugin (`enforce: "post"`) | Add to the rsc project `plugins`. Runs the rango plugin's `"use cache"` transform with dev ids (`<path>#<name>`, the path relative to the root its `configResolved` reads) so a directive function caches. |
22
24
 
23
25
  ### Returns — `RangoTestConfig` (from `rangoTestConfig`)
24
26
 
@@ -53,6 +55,7 @@ import { defineConfig } from "vitest/config";
53
55
  import {
54
56
  rangoTestAliases,
55
57
  rangoUseClientTransform,
58
+ rangoUseCacheTransform,
56
59
  rangoInlineDeps,
57
60
  } from "@rangojs/router/testing/vitest";
58
61
 
@@ -60,7 +63,7 @@ import {
60
63
  process.env.NODE_ENV = "production";
61
64
 
62
65
  export default defineConfig({
63
- plugins: [rangoUseClientTransform()],
66
+ plugins: [rangoUseClientTransform(), rangoUseCacheTransform()],
64
67
  resolve: {
65
68
  conditions: ["react-server"],
66
69
  alias: rangoTestAliases({ preset: "cloudflare" }), // or { preset: "node" }
@@ -109,12 +112,13 @@ Scripts:
109
112
 
110
113
  - Node >= 23 requires `rangoTestConfig()`, not bare `rangoTestAliases()`. `@rangojs/router` is consumed as SOURCE (exports -> `./src/*.ts`), and Node >= 23 refuses to type-strip `.ts` under `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`). `rangoTestConfig` ships as compiled JS AND adds `server.deps.inline: [/@rangojs[/\\]router/]` so Vite (not Node) transpiles rango source. With bare `rangoTestAliases` you must wire `deps.inline` yourself.
111
114
  - Two separate projects. The node/DOM project keeps React on its CLIENT build; the Flight project uses the `react-server` condition in a separate `vitest.rsc.config.ts`. The main project must NOT set `react-server` — it flips React to the no-hooks server build and breaks every `renderRoute` / client test.
112
- - The rsc project needs BOTH `resolve.conditions: ["react-server"]` AND the bare `@rangojs/router` -> `index.rsc.ts` alias from `rangoTestAliases({ preset })`. `resolve.conditions` alone is not reliably applied to bare-package export resolution; without the alias a handler/component reading `getRequestContext()` / `cookies()` resolves the throwing out-of-react-server stub (symptom: `renderHandler` returns `tree: undefined`). `renderToFlightString` / `renderServerTree` now self-diagnose this exact misconfiguration — they reject with an actionable message naming `rangoTestAliases`, rather than surfacing the opaque stub error.
115
+ - The rsc project needs BOTH `resolve.conditions: ["react-server"]` AND the bare `@rangojs/router` -> `index.rsc.ts` alias from `rangoTestAliases({ preset })`. `resolve.conditions` alone is not reliably applied to bare-package export resolution; without the alias a handler/component reading `getRequestContext()` / `cookies()` resolves the throwing out-of-react-server stub. `renderToFlightString`, `renderServerTree`, and `renderHandler` all detect this exact misconfiguration and reject with an actionable message naming `rangoTestAliases`, rather than surfacing the opaque stub error.
113
116
  - `NODE_ENV` must be `"production"` in the rsc project. Dev `NODE_ENV` crashes the bare worker (jsxDEV owner-stack machinery uninitialized) and emits volatile debug rows that defeat stable Flight snapshots.
114
117
  - The forked rsc worker (`pool: "forks"`) must force the condition via `execArgv: ["--conditions=react-server"]`, or React throws "the react-server condition must be enabled".
115
118
  - The `@rangojs/router:version` and `@vitejs/plugin-rsc/rsc` (`/rsc/server`, `/rsc/client`) virtuals must be stubbed; the preset does it. A bare router import without stubbing throws.
116
119
  - The rango fragment goes under `test` (`test.alias` + `test.server.deps.inline`, both returned by `rangoTestConfig`), NOT under top-level `resolve`.
117
120
  - Wire `rangoUseClientTransform()` into the rsc project `plugins` so islands auto-discover from the server tree imports (see `./server-tree.md`); without it, register islands explicitly with `clientComponents`. The transform's `registerClientReference` import is resolved from `@rangojs/router`'s `@vitejs/plugin-rsc` — the consumer does not need a direct plugin-rsc dependency.
121
+ - Wire `rangoUseCacheTransform()` into the rsc project `plugins` so a function written with `"use cache"` is wrapped as in dev. `rango()` runs that transform only in its `rsc` environment and Vitest transforms in `ssr`, so without it the directive is inert and the function runs on every call. See [`./render-handler.md`](./render-handler.md#a-use-cache-hit).
118
122
 
119
123
  ## See also
120
124
 
@@ -1,82 +1,207 @@
1
1
  ---
2
2
  name: theme
3
- description: Opt-in theme system with FOUC prevention for light/dark mode. Use when adding a light/dark mode toggle, or the page flashes the wrong theme on load (FOUC).
3
+ description: Opt-in light/dark theme system with FOUC prevention, server-side theme access, and a useTheme() hook. Use when adding a light/dark mode toggle, reading or setting the theme in handlers or middleware, or when the page flashes the wrong theme on load (FOUC).
4
4
  argument-hint: [setup]
5
5
  ---
6
6
 
7
7
  # Theme Support
8
8
 
9
- Opt-in theme system with FOUC prevention.
9
+ Opt-in theme system. Enabling it gives you:
10
+
11
+ - an inline script (rendered by `<Html.Meta />`) that applies the stored theme
12
+ to `<html>` before first paint, so there is no flash of the wrong theme;
13
+ - `ctx.theme` / `ctx.setTheme()` in handlers and middleware;
14
+ - the `useTheme()` hook in client components (the provider is added for you).
15
+
16
+ The theme is stored in a cookie (so the server can read it) and in
17
+ localStorage.
10
18
 
11
19
  ## Enable
12
20
 
13
21
  ```typescript
14
22
  import { createRouter } from "@rangojs/router";
15
23
 
16
- // Simple - all defaults
17
- const router = createRouter<Env>({
24
+ // All defaults
25
+ export const router = createRouter<AppBindings>({
18
26
  document: Document,
19
27
  urls: urlpatterns,
20
28
  theme: true,
21
29
  });
22
30
 
23
- // Custom config
24
- const router = createRouter<Env>({
31
+ // Custom config (values shown are the defaults unless noted)
32
+ export const router = createRouter<AppBindings>({
25
33
  document: Document,
26
34
  urls: urlpatterns,
27
35
  theme: {
28
36
  defaultTheme: "system", // "light" | "dark" | "system"
29
37
  themes: ["light", "dark"],
30
- attribute: "class", // or "data-theme"
31
- storageKey: "theme",
38
+ attribute: "class", // or any "data-*" attribute, e.g. "data-theme"
39
+ storageKey: "theme", // cookie and localStorage key
40
+ enableSystem: true, // resolve "system" via prefers-color-scheme
41
+ enableColorScheme: true, // set style="color-scheme: ..." on <html>
42
+ value: { dark: "theme-dark" }, // optional: attribute value per theme (not a default)
32
43
  },
33
44
  });
34
45
  ```
35
46
 
36
- ## Server (in handlers/middleware)
47
+ With `attribute: "class"` the resolved theme is added as a class on `<html>`
48
+ (`<html class="dark">`); with a `data-*` attribute it is set as that
49
+ attribute's value (`<html data-theme="dark">`). `"system"` resolves to
50
+ `"light"` or `"dark"` before it is applied. With `enableSystem: false`,
51
+ `"system"` is not a valid theme and a `"system"` default falls back to the
52
+ first entry in `themes`.
53
+
54
+ ## Document requirements
55
+
56
+ - Render `<Html.Meta />` in `<head>`: it emits the FOUC-prevention script
57
+ first, before the meta tags. The default Document already does this.
58
+ - Add `suppressHydrationWarning` to `<html>`: the script changes its
59
+ `class`/`style` before React hydrates.
60
+
61
+ ```tsx
62
+ "use client";
63
+ import type { ReactNode } from "react";
64
+ import { Html } from "@rangojs/router/client";
65
+
66
+ export function Document({ children }: { children: ReactNode }) {
67
+ return (
68
+ <html lang="en" suppressHydrationWarning>
69
+ <head>
70
+ <Html.Meta />
71
+ </head>
72
+ <body>{children}</body>
73
+ </html>
74
+ );
75
+ }
76
+ ```
77
+
78
+ A Document that does not render `<Html.Meta />` can place `<ThemeScript />`
79
+ from `@rangojs/router/theme` in `<head>` instead. It takes the resolved config
80
+ (`config: ResolvedThemeConfig`) and an optional `nonce`.
81
+
82
+ ## Server (handlers and middleware)
83
+
84
+ `ctx.theme` is the stored theme, or `defaultTheme` when there is none.
85
+ `ctx.setTheme(theme)` sets the theme cookie on the response; invalid values
86
+ are rejected with a console warning. Both are typed optional because they only
87
+ exist when `theme` is configured.
88
+
89
+ `ctx.theme` is the visitor's theme cookie, so a handler reads it only on a
90
+ route whose output is rendered per request:
37
91
 
38
92
  ```typescript
39
93
  import type { Middleware } from "@rangojs/router";
40
94
 
41
- // In a handler
95
+ // In a handler on a route without ppr or cache()
42
96
  path("/settings", (ctx) => {
43
- const currentTheme = ctx.theme; // read from cookie
97
+ const currentTheme = ctx.theme; // "light" | "dark" | "system" | undefined
44
98
  return <SettingsPage theme={currentTheme} />;
45
99
  });
46
100
 
47
- // In middleware
48
- export const themeMiddleware: Middleware = async (ctx, next) => {
49
- // Set theme based on user preference
50
- ctx.setTheme("dark");
101
+ // In middleware: apply a theme chosen through a query param
102
+ export const themeFromQuery: Middleware = async (ctx, next) => {
103
+ const requested = ctx.url.searchParams.get("theme");
104
+ if (requested === "light" || requested === "dark") {
105
+ ctx.setTheme?.(requested);
106
+ }
51
107
  await next();
52
108
  };
53
109
  ```
54
110
 
55
- Theme accessors are handler/middleware-only — the loader context has no
56
- `ctx.theme`/`ctx.setTheme`. A loader that needs the theme reads the cookie
57
- directly via `cookies()` (loaders always run fresh, so the read is safe).
111
+ ### On `ppr` and `cache()` routes
112
+
113
+ A `ppr` shell and a `cache()` entry are shared by every visitor, so a handler
114
+ there must not read the visitor's theme. `ctx.theme` (handler and middleware)
115
+ and `getRequestContext().theme` are guarded like `cookies()`:
116
+
117
+ | Where the theme is read | What happens |
118
+ | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
119
+ | a handler on a `ppr` route | The shell capture is refused (warned once per URL); every request stays a MISS |
120
+ | inside a `cache()` boundary | Throws on a cache miss, like `cookies()` |
121
+ | inside a `"use cache"` function (handler or middleware) | Throws; read it outside and pass it in as an argument |
122
+ | after `ctx.dynamic()` on `ppr` | Allowed: that render is never captured |
123
+ | middleware `ctx.set()` of the theme, read by a handler | Not guarded: the first visitor's theme bakes into the shell or `cache()` entry, like a session object |
124
+ | a route with no `ppr` or `cache()` above it, or middleware | Allowed: rendered per request |
125
+ | a loader with its own `cache()` and no `key()` | The miss fails and stores nothing (#972), like a `cookies()` read there; key the theme |
126
+
127
+ The theme getters are read-only and non-enumerable: `{ ...ctx }` and
128
+ `Object.assign({}, ctx)` do not carry `theme`, and assigning it throws.
129
+
130
+ `ctx.setTheme()` throws in a handler on these routes too, like any response
131
+ write there; call it from middleware.
132
+
133
+ Read the theme where it is per request instead:
134
+
135
+ - `useTheme()` in a client component. The `<html>` class is right before
136
+ paint (the theme script sets it). On a `ppr` HIT the shell carries the
137
+ no-cookie default (`defaultTheme`), whoever captured it: a visitor with no
138
+ stored theme sees the default, and one with a stored theme sees it after
139
+ hydration.
140
+ - A live loader (no `ssr: false`). Loaders do not get `ctx.theme` or
141
+ `ctx.setTheme`; read the cookie directly. A live loader runs on every
142
+ request, HITs included, and on a `ppr` route needs `loading()` or an inline
143
+ `<Suspense>` above its reader. A bake-lane loader (`ssr: false`) runs at
144
+ capture, where this `cookies()` read refuses the capture, and so does a
145
+ loader a handler awaits with `ctx.use()`.
146
+
147
+ ```typescript
148
+ import { cookies, createLoader } from "@rangojs/router";
149
+
150
+ export const ThemedLoader = createLoader(async () => {
151
+ const theme = cookies().get("theme")?.value ?? "system"; // storageKey
152
+ return loadThemedAssets(theme);
153
+ });
154
+ ```
155
+
156
+ If you bind it with its own `cache()`, the entry is shared across users, so the
157
+ miss fails unless it has a `key()`. Any `key()` switches that check off, so
158
+ make it include the theme:
159
+
160
+ ```typescript
161
+ loader(ThemedLoader, () => [
162
+ cache({
163
+ ttl: 300,
164
+ key: () => `themed:${cookies().get("theme")?.value ?? "system"}`,
165
+ }),
166
+ ]);
167
+ ```
58
168
 
59
169
  ## Client
60
170
 
61
171
  ```tsx
62
172
  "use client";
63
- import { useTheme } from "@rangojs/router/theme";
173
+ import { useTheme, type Theme } from "@rangojs/router/theme";
64
174
 
65
- function ThemeToggle() {
66
- const { theme, setTheme, resolvedTheme, systemTheme, themes } = useTheme();
175
+ export function ThemeToggle() {
176
+ const { theme, setTheme, resolvedTheme, themes } = useTheme();
67
177
 
68
178
  return (
69
- <select value={theme} onChange={(e) => setTheme(e.target.value)}>
70
- <option value="system">System</option>
71
- <option value="light">Light</option>
72
- <option value="dark">Dark</option>
73
- </select>
179
+ <label>
180
+ Theme ({resolvedTheme})
181
+ <select value={theme} onChange={(e) => setTheme(e.target.value as Theme)}>
182
+ {themes.map((t) => (
183
+ <option key={t} value={t}>
184
+ {t}
185
+ </option>
186
+ ))}
187
+ </select>
188
+ </label>
74
189
  );
75
190
  }
76
191
  ```
77
192
 
78
- ## Notes
193
+ `useTheme()` returns:
194
+
195
+ - `theme`: the stored setting (`"light" | "dark" | "system"`)
196
+ - `setTheme(theme)`: update the theme; writes the cookie and localStorage
197
+ - `resolvedTheme`: what is actually shown (`"system"` resolved)
198
+ - `systemTheme`: the current OS preference (`"light" | "dark"`)
199
+ - `themes`: the configured themes, with `"system"` first when `enableSystem` is on
200
+
201
+ `useTheme()` throws when `theme` is not enabled in `createRouter()`.
202
+
203
+ ## Related
79
204
 
80
- - `<MetaTags />` auto-renders inline script for FOUC prevention
81
- - Add `suppressHydrationWarning` to `<html>`
82
- - Theme persists in localStorage + cookie (for SSR)
205
+ - `/tailwind`: pairing `attribute: "class"` with Tailwind's `dark:` variant
206
+ - `/css`: where document stylesheets go
207
+ - `/router-setup`: the Document component
@@ -1,17 +1,27 @@
1
1
  ---
2
2
  name: typesafety
3
- description: Set up type-safe routes, params, and environment types in @rangojs/router. Use when route or search params aren't typed, TypeScript can't infer a loader's return type, or wiring up typed environment bindings.
3
+ description: Set up type-safe route names, params, search params, response payloads, and environment types in @rangojs/router. Use when route or search params aren't typed, ctx.env or ctx.get() is untyped, TypeScript can't infer a loader's return type, or wiring RegisteredRoutes / the generated route map.
4
4
  argument-hint: [setup]
5
5
  ---
6
6
 
7
7
  # Type Safety Setup
8
8
 
9
- @rangojs/router provides end-to-end type safety for routes, parameters, and
10
- environment. Without it: `ctx.reverse()`/`href()` accept any string (typos
11
- 404 at runtime, not compile time), `ctx.search`/`ctx.params` fall back to
12
- loose `Record<string, string>`, and `ctx.env`/`ctx.get()` are untyped so a
13
- missing binding surfaces as `undefined` in production instead of a build
14
- error.
9
+ This skill covers how route names, params, search params, response payloads,
10
+ environment bindings, and context variables get their types. Use it when
11
+ setting up a new app's type surfaces or when one of them resolves to
12
+ `any`/`unknown`/`never` unexpectedly.
13
+
14
+ What you lose without the setup:
15
+
16
+ - `ctx.reverse()` accepts any name and `href()` any `/${string}` path, so a
17
+ typo 404s at runtime instead of failing to compile.
18
+ - `Handler<"name">` is rejected (there is no generated map to look the name
19
+ up in), so `ctx.params`/`ctx.search` can't be inferred from the route.
20
+ - `ctx.env` is `unknown` until `Rango.Env` is registered (every binding access
21
+ is a compile error), and string-key `ctx.get("key")` returns `any` until
22
+ `Rango.Vars` is registered (a typo'd key is silently accepted).
23
+ - `Rango.PathResponse` resolves to `never` until `Rango.RegisteredRoutes` is
24
+ registered.
15
25
 
16
26
  Each topic's full setup, code, and caveats live in a companion file linked
17
27
  below. Read the one for your case.
@@ -48,5 +58,6 @@ below. Read the one for your case.
48
58
  scoped context tokens, handle typing, passing loaders/handles as typed
49
59
  props, and location state typing.
50
60
 
51
- See `/links` for the full URL generation guide (per-module `*.gen.ts`,
52
- `useReverse`).
61
+ Related skills: `/links` (URL generation: `ctx.reverse`, `href`,
62
+ `useReverse`, per-module `*.gen.ts`), `/response-routes` (response payload
63
+ typing), `/api-client` (typed fetch client over `RouteResponse`).