@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
@@ -9,8 +9,8 @@ argument-hint:
9
9
  Use this when a cached or prerendered shell has dynamic holes, and the live
10
10
  data layer needs to know **what the shell actually contains** — which
11
11
  products, which slots, which keys. The frozen render describes itself
12
- through a handle; loaders (always live) read that description and fetch
13
- exactly the dynamic data the shell needs, in one batch.
12
+ through a handle; live loaders (no `ssr: false`) read that description and
13
+ fetch exactly the dynamic data the shell needs, in one batch.
14
14
 
15
15
  Canonical case: a prerendered product list where prices must stay live.
16
16
 
@@ -32,14 +32,19 @@ batched.
32
32
  ## The mechanism (three features composed)
33
33
 
34
34
  1. **Handles record data at render time.** The handler pushes to a handle
35
- (`ctx.use(Handle)`) while it renders — at build time for `Prerender`, on
36
- the cache miss for `cache()`. (Loader bodies can push handles too — see
37
- `/loader` — but a loader push is request-time and is NOT part of the
38
- replayed artifact; a manifest handle must be pushed by the code that gets
39
- frozen with the shell.)
35
+ (`ctx.use(Handle)`) while it renders — at build time for `Prerender`, on the
36
+ cache miss for `cache()`. (Loader bodies can push handles too — see `/loader`
37
+ — but a live loader's push is request-time and is NOT part of the replayed
38
+ artifact; a manifest handle must be pushed by the code that gets frozen with
39
+ the shell. An `ssr: false` loader on a `ppr` route is the exception: its
40
+ settled pushes are recorded with the shell. On a HIT, one whose return has no
41
+ promises does not run (unless the capture marked it to run), so the recorded
42
+ pushes are what the page shows; one whose return carries promises runs on
43
+ every HIT, and its pushes replace the recorded ones — `/ppr` "On a shell
44
+ HIT".)
40
45
  2. **Replay on every hit.** Handle data is stored with the Flight payload
41
- and replayed into the handle store on cache/prerender hits — handler code
42
- does not re-run, but its pushes do.
46
+ and replayed into the handle store on cache, prerender and `ppr` shell
47
+ hits — handler code does not re-run, but its pushes do.
43
48
  3. **Loaders read after the render barrier.** A DSL loader can
44
49
  `await ctx.rendered()` (waits for all non-loader segments to settle —
45
50
  fresh render or replay alike), then `ctx.get(Handle)` returns the
@@ -94,7 +99,7 @@ import { RenderedProducts } from "../handles/rendered-products";
94
99
  export const PriceLoader = createLoader(async (ctx) => {
95
100
  await ctx.rendered();
96
101
  const ids = ctx.get(RenderedProducts);
97
- return db.pricesFor(ids); // Map<string, number> keyed by product id
102
+ return db.pricesFor(ids); // Record<string, number> keyed by product id
98
103
  });
99
104
  ```
100
105
 
@@ -138,9 +143,11 @@ cache({ ttl: 600, tags: ["products"] }, () => [
138
143
  ## Contract and gotchas
139
144
 
140
145
  - **The manifest is exactly as fresh as the shell.** Replayed handle data is
141
- frozen with the payload. To change _which_ products render, invalidate the
142
- shell (`updateTag("products")`, TTL expiry, rebuild) — never treat the
143
- loader as the refresh path for the list itself. This is the point:
146
+ frozen with the payload. To change _which_ products render, refresh the
147
+ shell — for `cache()`, `updateTag("products")` or TTL expiry; for
148
+ `Prerender`, a rebuild (tag invalidation does not reach build-time payloads,
149
+ see `/prerender`). Never treat the loader as the refresh path for the list
150
+ itself. This is the point:
144
151
  shell and holes cannot desync because they share one artifact.
145
152
  - **No request-scoped data in a manifest handle.** The handle data is baked
146
153
  into a shared artifact — the same cross-user rule as any cached content.
@@ -181,12 +188,19 @@ const prices = await runLoader(PriceLoader, {
181
188
 
182
189
  This tests the loader's post-barrier logic. The real
183
190
  push → store → replay → barrier wiring is covered at the e2e tier (dev +
184
- production), like every cache-path behavior.
191
+ production), like every cache-path behavior. On a `ppr` route,
192
+ `serveShellRequest(router, url, { cacheStore })` from
193
+ `@rangojs/router/testing/flight` runs the real capture and HIT in a unit test
194
+ (react-server Vitest): serve the URL twice with the same store; the second
195
+ response is a HIT, and its `flight` carries the live loader's output for the
196
+ replayed ids. Call `resetShellTestState()` in `beforeEach`. Recipe:
197
+ `/testing` → `serve-shell-request.md`.
185
198
 
186
199
  ## Related
187
200
 
188
201
  - `/prerender` — `Prerender`/`Passthrough`, build flow, passthrough fallback
189
202
  - `/caching` — segment `cache()`, stores, tags
203
+ - `/ppr` — cached HTML shells; its holes are the same live loaders
190
204
  - `/loader` — loader context, `ctx.rendered()`, streaming
191
205
  - `/hooks` — `useHandle` for reading handle data in client components
192
206
  - `/rango` → "Passing data down the tree" — this pattern is the frozen→live
@@ -38,11 +38,12 @@ export const urlpatterns = urls(({ path }) => [
38
38
  "/events/ticks",
39
39
  (ctx) => {
40
40
  const encoder = new TextEncoder();
41
+ let interval: ReturnType<typeof setInterval> | undefined;
41
42
 
42
43
  const stream = new ReadableStream({
43
- async start(controller) {
44
+ start(controller) {
44
45
  let count = 0;
45
- const interval = setInterval(() => {
46
+ interval = setInterval(() => {
46
47
  controller.enqueue(
47
48
  encoder.encode(`event: tick\ndata: ${++count}\n\n`),
48
49
  );
@@ -51,9 +52,17 @@ export const urlpatterns = urls(({ path }) => [
51
52
  // Honor client disconnect — signal comes from ctx.request.signal
52
53
  ctx.request.signal.addEventListener("abort", () => {
53
54
  clearInterval(interval);
54
- controller.close();
55
+ try {
56
+ controller.close();
57
+ } catch {
58
+ // already closed or cancelled
59
+ }
55
60
  });
56
61
  },
62
+ // The runtime cancels the stream when the reader goes away
63
+ cancel() {
64
+ clearInterval(interval);
65
+ },
57
66
  });
58
67
 
59
68
  return new Response(stream, {
@@ -80,18 +89,21 @@ source.addEventListener("tick", (e) => console.log("tick", e.data));
80
89
 
81
90
  ### SSE caveats
82
91
 
83
- - **Never wrap SSE routes in `cache()`** — a cached `ReadableStream` is read
84
- once and would replay an empty body on the next hit. `path.stream` is
85
- already excluded from response-route caching, but don't layer a custom
86
- cache() middleware on top.
92
+ - **Never put an SSE route inside a `cache()` scope.** Response-route
93
+ `cache()` does not special-case `path.stream`: an SSE response is a `200`
94
+ like any other, so a `cache()` above it would try to store the stream. Keep
95
+ SSE routes outside every `cache()` boundary, and don't layer a custom
96
+ caching middleware on top either.
87
97
  - **Middleware is fine.** Global/route middleware rewraps the SSE `Response`
88
98
  as `new Response(response.body, { status, headers })` to merge stub headers.
89
99
  The `ReadableStream` body is passed by reference, not consumed, so the
90
100
  client sees the stream unchanged. (WebSocket upgrades are the exception —
91
101
  those bypass rewrap entirely; see below.)
92
- - **Honor `ctx.request.signal`.** Without wiring abort to your source
93
- (timer, DB cursor, upstream fetch), the stream leaks when the client
94
- disconnects.
102
+ - **Honor `ctx.request.signal` and `cancel()`.** Without wiring disconnect to
103
+ your source (timer, DB cursor, upstream fetch), the stream leaks when the
104
+ client goes away. Clean up in both places: depending on the runtime, a
105
+ disconnect surfaces as the request signal aborting, the stream being
106
+ cancelled, or both.
95
107
  - **Disable Nginx/CDN buffering** via `x-accel-buffering: no` and ensure
96
108
  no intermediate proxy rebuffers. On Cloudflare Workers this is a non-issue.
97
109
 
@@ -210,6 +222,8 @@ HTTP request instead (e.g. during login), then read them at upgrade time
210
222
  via `ctx.request.headers.get("cookie")`.
211
223
 
212
224
  ```typescript
225
+ import { cookies } from "@rangojs/router";
226
+
213
227
  // Avoid: this cookie may not land on the upgrade response, and the client
214
228
  // never reads it during the handshake regardless.
215
229
  router.use(async (ctx, next) => {
@@ -219,7 +233,7 @@ router.use(async (ctx, next) => {
219
233
 
220
234
  // Prefer: authenticate by reading a cookie set on a prior HTTP request.
221
235
  path.any("/ws", (ctx) => {
222
- const session = parseCookie(ctx.request.headers.get("cookie"))?.session;
236
+ const session = cookies().get("session")?.value;
223
237
  if (!verify(session)) return new Response("unauthorized", { status: 401 });
224
238
  // ...upgrade
225
239
  });
@@ -240,7 +254,8 @@ router.use(async (ctx, next) => {
240
254
 
241
255
  ## Caching
242
256
 
243
- - **SSE** — do not combine with `cache()` (streams can't be replayed).
257
+ - **SSE** — do not place under `cache()`: it is a cacheable `200`, and an
258
+ endless stream cannot be stored or replayed.
244
259
  - **WebSocket** — `cache()` is inert because only `status === 200` is cacheable.
245
260
 
246
261
  ## Runtime caveats
@@ -279,5 +294,8 @@ Object.defineProperty(upgrade, "webSocket", {
279
294
 
280
295
  ## See also
281
296
 
282
- - `response-routes` — the parent skill for `path.json/text/html/stream/any`.
283
- - `middleware` — how global and route-level middleware compose with handlers.
297
+ - `/response-routes` — the parent skill for `path.json/text/html/stream/any`
298
+ and `ResponseHandlerContext` (`ctx.header()`, `ctx.waitUntil()`,
299
+ `ctx.executionContext`).
300
+ - `/middleware` — how global and route-level middleware compose with handlers.
301
+ - `/cloudflare` — Workers bindings and Durable Object setup.
@@ -6,7 +6,9 @@ argument-hint: [setup]
6
6
 
7
7
  # Tailwind CSS
8
8
 
9
- Set up Tailwind CSS v4 with the Rango router. Styles are loaded through the Document component using Vite's `?url` CSS import.
9
+ Set up Tailwind CSS v4 in a Rango app: the Vite plugin, the CSS entry, loading
10
+ it from the Document with Vite's `?url` import, theme tokens, class-based dark
11
+ mode, and fonts. The general stylesheet pattern is explained in `/css`.
10
12
 
11
13
  ## Install
12
14
 
@@ -18,11 +20,14 @@ pnpm add -D tailwindcss @tailwindcss/vite
18
20
 
19
21
  ```typescript
20
22
  // vite.config.ts
23
+ import { defineConfig } from "vite";
21
24
  import tailwindcss from "@tailwindcss/vite";
25
+ import { rango } from "@rangojs/router/vite";
22
26
 
23
27
  export default defineConfig({
24
28
  plugins: [
25
29
  tailwindcss(),
30
+ rango(),
26
31
  // ... other plugins
27
32
  ],
28
33
  });
@@ -37,18 +42,17 @@ export default defineConfig({
37
42
 
38
43
  ## Document Component
39
44
 
40
- Import the CSS file with `?url` to get a hashed URL, then preload and link it in
41
- `<head>`. Give the `<link rel="stylesheet">` a `precedence` prop so React 19
42
- manages it as a resource — de-duped by `href`, ordered, and loaded before paint
43
- (no flash of unstyled content). See
44
- [Stylesheets and cross-app navigation](#stylesheets-and-cross-app-navigation):
45
+ Import the CSS file with `?url` to get its hashed URL, then preload and link it
46
+ in `<head>`. Give the `<link rel="stylesheet">` a `precedence` prop so React 19
47
+ manages it as a resource: de-duped by `href`, ordered, and loaded before paint
48
+ (no flash of unstyled content).
45
49
 
46
50
  ```tsx
47
51
  // src/document.tsx
48
52
  "use client";
49
53
 
50
54
  import type { ReactNode } from "react";
51
- import { MetaTags } from "@rangojs/router/client";
55
+ import { Html } from "@rangojs/router/client";
52
56
  import styles from "./index.css?url";
53
57
 
54
58
  export function Document({ children }: { children: ReactNode }) {
@@ -57,7 +61,7 @@ export function Document({ children }: { children: ReactNode }) {
57
61
  <head>
58
62
  <link rel="preload" href={styles} as="style" precedence="default" />
59
63
  <link rel="stylesheet" href={styles} precedence="default" />
60
- <MetaTags />
64
+ <Html.Meta />
61
65
  </head>
62
66
  <body className="font-sans antialiased text-slate-900 bg-slate-50">
63
67
  {children}
@@ -67,27 +71,15 @@ export function Document({ children }: { children: ReactNode }) {
67
71
  }
68
72
  ```
69
73
 
70
- ## Stylesheets and cross-app navigation
74
+ The `?url` suffix tells Vite to return the processed CSS file's URL instead of
75
+ injecting it as a side effect, giving a stable, hashed asset path in both
76
+ development and production. A bare `import "./index.css"` also produces managed
77
+ CSS, but it is not guaranteed to be in the initial streamed `<head>`; prefer
78
+ `?url` + `<link precedence>` for document CSS (see `/css`).
71
79
 
72
- The `precedence` prop opts a `<link rel="stylesheet">` into React 19's managed
73
- stylesheet model — React de-duplicates it by `href`, orders it by precedence, and
74
- loads it before paint (avoiding a flash of unstyled content). It is the
75
- recommended way to render a stylesheet link, which is why the example above uses
76
- it. (A bare side-effect `import "./index.css"` also produces managed CSS via
77
- `@vitejs/plugin-rsc`, but carries an SSR-streaming caveat — prefer the `?url` +
78
- `<link precedence>` form for document CSS. See `/css`.)
79
-
80
- For **host-router** apps (`/host-router`), a client-side navigation that crosses
81
- an app boundary is a **full document load**, not a soft swap — the framework
82
- redirects on an app switch. So each app's document (its stylesheets, theme, meta)
83
- is always re-established cleanly by the target app's own load; you do not have to
84
- coordinate stylesheet `href`s or `precedence` across apps. (This replaced an
85
- earlier soft cross-app swap, under which a stylesheet shared across apps — every
86
- app's `@import "tailwindcss"` compiles to one hashed asset — could be dropped by
87
- React's by-`href` resource dedup if the apps disagreed on `precedence`. The full
88
- reload removes that footgun entirely.)
89
-
90
- The `?url` suffix tells Vite to return the processed CSS file's URL instead of injecting it as a side effect. This gives you a stable, hashed asset path that works in both development and production.
80
+ For **host-router** apps (`/host-router`), navigating between apps is a full
81
+ document load, so each app's Tailwind stylesheet is loaded by its own Document;
82
+ there is nothing to coordinate across apps.
91
83
 
92
84
  ## Customizing the Theme
93
85
 
@@ -107,17 +99,29 @@ Tailwind v4 uses CSS `@theme` for customization:
107
99
 
108
100
  ## Dark Mode
109
101
 
110
- Combine with the Rango theme system (see `/theme`):
102
+ Combine with the Rango theme system (see `/theme`). With `attribute: "class"`
103
+ (the default) the theme script puts `class="dark"` or `class="light"` on
104
+ `<html>` before first paint; add `suppressHydrationWarning` to the Document's
105
+ `<html>` so React accepts that change:
111
106
 
112
107
  ```typescript
113
- const router = createRouter({
108
+ export const router = createRouter({
114
109
  document: Document,
115
110
  urls: urlpatterns,
116
111
  theme: { attribute: "class" },
117
112
  });
118
113
  ```
119
114
 
120
- Then use Tailwind's `dark:` variant which reads the `class` attribute:
115
+ Tailwind v4's `dark:` variant follows `prefers-color-scheme` by default. Point
116
+ it at the class instead, so the user's stored choice wins over the OS setting:
117
+
118
+ ```css
119
+ /* src/index.css */
120
+ @import "tailwindcss";
121
+ @custom-variant dark (&:where(.dark, .dark *));
122
+ ```
123
+
124
+ Then use the `dark:` variant as usual:
121
125
 
122
126
  ```tsx
123
127
  <div className="bg-white dark:bg-slate-900 text-slate-900 dark:text-white">
@@ -147,7 +151,7 @@ No extra `<link>` tags needed in the Document -- Vite bundles the font files fro
147
151
 
148
152
  ## Notes
149
153
 
150
- - `?url` import is required -- bare CSS imports inject styles as a side effect and do not work with SSR streaming
151
- - `<link rel="preload" as="style">` eliminates render-blocking by starting the download early
154
+ - Use the `?url` import for the document stylesheet: a bare side-effect import is not guaranteed to be in the initial streamed `<head>` (see `/css`)
155
+ - `<link rel="preload" as="style">` starts the stylesheet download early
152
156
  - Tailwind v4 does not need a `tailwind.config.js` -- use `@theme` in CSS instead
153
157
  - The `@tailwindcss/vite` plugin handles content detection automatically
@@ -1,18 +1,24 @@
1
1
  ---
2
2
  name: testing
3
- description: Test @rangojs/router apps — unit (loaders/middleware/reverse/components), integration (dispatch/Flight), and e2e (dev+prod parity, progressive enhancement). Use when writing a unit test for a loader or middleware, or asking how to test a route end-to-end in dev and production.
3
+ description: Test @rangojs/router apps — unit (loaders, middleware, server actions, handles, transition/clientUrls predicates), client components (renderRoute), RSC/Flight (renderServerTree, renderHandler, serveShellRequest for PPR shells), integration (dispatch), and e2e (dev+prod parity, progressive enhancement). Use when writing a unit test for a loader, middleware, action, or handler, setting up vitest for a Rango app, or testing a route end-to-end in dev and production.
4
4
  argument-hint: [layer]
5
5
  ---
6
6
 
7
7
  # Testing @rangojs/router apps
8
8
 
9
+ This skill picks the right test layer for a behavior in a Rango app and gives
10
+ the API, a recipe, and the caveats for each testing primitive. Use it when
11
+ adding or changing tests, or when wiring the vitest/Playwright setup.
12
+
9
13
  Rango ships six consumer-facing testing entries, one per test runtime/dependency:
10
14
  `@rangojs/router/testing` (unit + integration, under a Vite-driven Vitest
11
15
  project), `@rangojs/router/testing/vitest` (the `rangoTestConfig`/`rangoTestAliases`
12
16
  setup preset), `@rangojs/router/testing/dom` (`renderRoute`, needs RTL + a DOM
13
17
  env), `@rangojs/router/testing/e2e` (the Playwright harness),
14
18
  `@rangojs/router/testing/flight` (real Flight, react-server condition only), and
15
- `@rangojs/router/testing/flight-matchers` (the Flight matchers).
19
+ `@rangojs/router/testing/flight-matchers` (the Flight matchers). Host-router
20
+ pattern helpers live separately at `@rangojs/router/host/testing` (see
21
+ `/host-router`).
16
22
 
17
23
  The hard problem in an RSC app is that the layer you reach for is dictated by
18
24
  **what the behavior touches** — a pure predicate is a one-line vitest test; a real
@@ -42,8 +48,8 @@ Use this skill when adding or changing tests for a Rango app: a loader,
42
48
  middleware, a server action, a route map, a client component, a response route,
43
49
  cache/SWR behavior, prerender, or a navigation/PE flow.
44
50
 
45
- Two non-negotiable mandates (from the repo's `CLAUDE.md`, and they apply to
46
- consumer apps too):
51
+ Two non-negotiable mandates (the Rango repo holds itself to these, and they
52
+ apply to consumer apps too):
47
53
 
48
54
  - **Every e2e covers BOTH dev and production.** A dev-only e2e is not acceptable.
49
55
  Use `parityDescribe` — it generates the dev and production describes from one
@@ -54,8 +60,10 @@ consumer apps too):
54
60
 
55
61
  ## The read-first shape
56
62
 
57
- Four import roots, each matched to the dependency/runtime that can load it — this
58
- split is forced by hard walls, not preference:
63
+ Four of the six entries hold the test primitives, each matched to the
64
+ dependency/runtime that can load it — this split is forced by hard walls, not
65
+ preference. (The other two are helpers: `/testing/vitest` is config-only, and
66
+ `/testing/flight-matchers` holds `toMatchFlight`/`toMatchFlightSnapshot`.)
59
67
 
60
68
  - `@rangojs/router/testing` — unit + integration primitives. Run these under a
61
69
  **Vite-driven Vitest** project with the rango Vite plugin active (the router
@@ -81,26 +89,31 @@ The single rule that drives everything:
81
89
 
82
90
  Each primitive links to its sub-file (API + recipe + caveats).
83
91
 
84
- | The behavior is… | Layer | Primitive | Import root |
85
- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------- | -------------------------------- |
86
- | a pure function / `reverse` / `href` / a predicate (`revalidate`, `isAction`) | unit + types | [`reverse`/`@ts-expect-error`](./reverse-and-types.md) | `@rangojs/router/testing` |
87
- | one loader's data logic | unit (node) | [`runLoader`](./loader.md) | `@rangojs/router/testing` |
88
- | a loader's cookie / header / thrown `redirect()`/`notFound()` / handle-push output | unit (node) | [`runLoaderResult`](./loader.md) | `@rangojs/router/testing` |
89
- | one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md) | `@rangojs/router/testing` |
90
- | a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md) | `@rangojs/router/testing` |
91
- | a `transition({ when })` gate (keep/drop) against nav source / target / action metadata | unit (node) | `runTransitionWhen` (`{ kept, whenContext }`; pass `{ ppr: true }` for pre-handler timing) | `@rangojs/router/testing` |
92
- | a `clientUrls()` `revalidate()` / `isAction(ref)` predicate | unit (node) | `runClientRevalidate` (production `makeIsAction` + locked defaults) | `@rangojs/router/testing` |
93
- | a handle's `collect`/accumulator, a seeded handle read, or a loader handle write | unit | [`collectHandle` / seeded `handles` / `handlePushes`](./handles.md) | `@rangojs/router/testing` |
94
- | a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
95
- | a redirect / status / headers / cookies / **response route** (json/text/html/xml/md), no Flight | integration | [`dispatch`](./response-routes.md) | `@rangojs/router/testing` |
96
- | a real async **Server Component** (assert what it rendered: typed boundary props, server-rendered host content, inlined-vs-island) | RSC unit | [`renderServerTree` + `findClientBoundaries`/`findElements`](./server-tree.md) | `@rangojs/router/testing/flight` |
97
- | the exact Flight **wire payload** shape (a drift snapshot) | RSC unit | [`renderToFlightString` + `toMatchFlightSnapshot`](./flight.md) | `@rangojs/router/testing/flight` |
98
- | a real route **handler** `(ctx) => rsc` (params/loaders/vars -> rendered RSC + effects) | RSC unit | [`renderHandler`](./render-handler.md) | `@rangojs/router/testing/flight` |
99
- | navigation, hydration, PE parity, view transitions, real SSR | e2e | [`createRangoE2E` -> `parityDescribe`/`expectParity`](./e2e-parity.md) | `@rangojs/router/testing/e2e` |
100
- | cache hit/miss/stale, prerender (= a cache hit by design) | e2e + signal | [`assertCacheStatus` (header) / `assertCacheDecision` (telemetry sink)](./cache-prerender.md) | `@rangojs/router/testing[/e2e]` |
101
- | PPR shell HIT/MISS (`ppr` path option), shell store key / getShell after flush | e2e + signal | [`assertShellStatus` / `shellCacheKey` + store](./cache-prerender.md#ppr-shell-x-rango-shell) | `@rangojs/router/testing[/e2e]` |
102
- | generated route map drift vs runtime | unit (node) | [`assertGeneratedRoutesMatch`](./reverse-and-types.md) | `@rangojs/router/testing` |
103
- | a platform binding (`env.DB` / Durable Object / `env.R2`) | unit/integr. | [your own double via `env`](./bindings.md) | (any primitive's `env` option) |
92
+ | The behavior is… | Layer | Primitive | Import root |
93
+ | ---------------------------------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------- | -------------------------------- |
94
+ | a `reverse` / `href` / params / env TYPE contract (a wrong name or missing param must not compile) | types (tsc) | [`@ts-expect-error` / `expectTypeOf`](./reverse-and-types.md) | `@rangojs/router/client` + `tsc` |
95
+ | one loader's data logic | unit (node) | [`runLoader`](./loader.md) | `@rangojs/router/testing` |
96
+ | a loader's cookie / header / thrown `redirect()`/`notFound()` / handle-push output | unit (node) | [`runLoaderResult`](./loader.md) | `@rangojs/router/testing` |
97
+ | one middleware's ordering / short-circuit / cookie+header merge | unit (node) | [`runMiddleware`](./middleware.md) | `@rangojs/router/testing` |
98
+ | a `"use server"` action's cookie / header / flash output (even on `throw redirect()`) | unit (node) | [`runInRequestContext`](./server-actions.md) | `@rangojs/router/testing` |
99
+ | a `transition({ when })` gate (keep/drop) against nav source / target / action metadata | unit (node) | [`runTransitionWhen`](./predicates.md) (`{ kept, whenContext }`; `{ ppr: true }` for pre-handler timing) | `@rangojs/router/testing` |
100
+ | a `clientUrls()` `revalidate()` / `isAction(ref)` predicate | unit (node) | [`runClientRevalidate`](./predicates.md) (production chain evaluator + locked defaults) | `@rangojs/router/testing` |
101
+ | a handle's `collect`/accumulator, a seeded handle read, or a loader handle write | unit | [`collectHandle` / seeded `handles` / `handlePushes`](./handles.md) | `@rangojs/router/testing` |
102
+ | a CLIENT component reading router context (`useParams`/`useReverse`/`Outlet`/`useNavigation`/`useLoader`) | unit (DOM) | [`renderRoute`](./client-components.md) | `@rangojs/router/testing/dom` |
103
+ | a stale indicator from `useLoader().isLoading` while a `transition()` navigation re-runs the loader | unit (DOM) | [`renderRoute` + `navigate(url, { loaders })`](./client-components.md#held-navigation) | `@rangojs/router/testing/dom` |
104
+ | a redirect / status / headers / cookies / **response route** (json/text/html/xml/md), no Flight | integration | [`dispatch`](./response-routes.md) | `@rangojs/router/testing` |
105
+ | a real async **Server Component** (assert what it rendered: typed boundary props, server-rendered host content, inlined-vs-island) | RSC unit | [`renderServerTree` + `findClientBoundaries`/`findElements`](./server-tree.md) | `@rangojs/router/testing/flight` |
106
+ | the exact Flight **wire payload** shape (a drift snapshot) | RSC unit | [`renderToFlightString` + `toMatchFlightSnapshot`](./flight.md) | `@rangojs/router/testing/flight` |
107
+ | a real route **handler** `(ctx) => rsc` (params/loaders/vars -> rendered RSC + effects) | RSC unit | [`renderHandler`](./render-handler.md) | `@rangojs/router/testing/flight` |
108
+ | navigation, hydration, PE parity, view transitions, real SSR | e2e | [`createRangoE2E` -> `parityDescribe`/`expectParity`](./e2e-parity.md) | `@rangojs/router/testing/e2e` |
109
+ | a `"use cache"` hit in a handler (the body runs once across renders; the value round-trips) | RSC unit | [`renderHandler` + seeded `cacheStore`](./render-handler.md#a-use-cache-hit) | `@rangojs/router/testing/flight` |
110
+ | a `"use cache"` hit in a loader (the same, in the react-server project) | RSC unit | [`runLoader` + seeded `cacheStore`](./loader.md) | `@rangojs/router/testing` |
111
+ | a loader's own `cache()` (HIT, `updateTag()` of a tag its body recorded) | RSC unit | [`runLoader` + `cache` + seeded `cacheStore`](./loader.md) | `@rangojs/router/testing` |
112
+ | cache hit/miss/stale, prerender (= a cache hit by design) | e2e + signal | [`assertCacheStatus` (header) / `assertCacheDecision` (telemetry sink)](./cache-prerender.md) | `@rangojs/router/testing[/e2e]` |
113
+ | a `ppr` route's shell: MISS -> capture -> HIT, what the shell froze vs what a HIT renders live, tag eviction, navigation replay | RSC unit | [`serveShellRequest`](./serve-shell-request.md) | `@rangojs/router/testing/flight` |
114
+ | PPR shell HIT/MISS in real HTML (`ppr` path option), shell store key / getShell after flush | e2e + signal | [`assertShellStatus` / `shellCacheKey` + store](./cache-prerender.md#ppr-shell-and-navigation-replay) | `@rangojs/router/testing[/e2e]` |
115
+ | generated route map drift vs runtime | unit (node) | [`assertGeneratedRoutesMatch`](./reverse-and-types.md) | `@rangojs/router/testing` |
116
+ | a platform binding (`env.DB` / Durable Object / `env.R2`) | unit/integr. | [your own double via `env`](./bindings.md) | (any primitive's `env` option) |
104
117
 
105
118
  Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
106
119
  `/handler-use`, `/hooks`, `/response-routes`, `/route`, `/caching`, `/prerender`,
@@ -111,22 +124,49 @@ Cross-references to the DSL skills: `/loader`, `/middleware`, `/server-actions`,
111
124
  - Cross-cutting: [`setup.md`](./setup.md), [`bindings.md`](./bindings.md)
112
125
  - Unit (node): [`loader.md`](./loader.md), [`middleware.md`](./middleware.md),
113
126
  [`server-actions.md`](./server-actions.md), [`handles.md`](./handles.md),
114
- [`reverse-and-types.md`](./reverse-and-types.md)
127
+ [`predicates.md`](./predicates.md), [`reverse-and-types.md`](./reverse-and-types.md)
115
128
  - Unit (DOM): [`client-components.md`](./client-components.md)
116
129
  - RSC unit: [`flight.md`](./flight.md), [`server-tree.md`](./server-tree.md),
117
- [`render-handler.md`](./render-handler.md)
130
+ [`render-handler.md`](./render-handler.md),
131
+ [`serve-shell-request.md`](./serve-shell-request.md)
118
132
  - Integration: [`response-routes.md`](./response-routes.md)
119
133
  - E2E: [`e2e-parity.md`](./e2e-parity.md), [`cache-prerender.md`](./cache-prerender.md)
120
134
 
121
- ## Pre-push checklist (mirror CLAUDE.md)
122
-
123
- Before pushing, run all of these and fix any failure:
135
+ ## Every export, by entry
136
+
137
+ Where each runtime export is documented (types such as `RunLoaderOptions` sit
138
+ beside their function):
139
+
140
+ | Entry | Exports | Documented in |
141
+ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
142
+ | `@rangojs/router/testing` | `runLoader`, `runLoaderResult` | [`loader.md`](./loader.md) |
143
+ | | `runMiddleware` | [`middleware.md`](./middleware.md) |
144
+ | | `runInRequestContext`, `createTestRequestContext`, `runWithRequestContext`, `toRequest`, `seedVariables` | [`server-actions.md`](./server-actions.md) |
145
+ | | `runTransitionWhen`, `runClientRevalidate` | [`predicates.md`](./predicates.md) |
146
+ | | `collectHandle` | [`handles.md`](./handles.md) |
147
+ | | `dispatch` | [`response-routes.md`](./response-routes.md) |
148
+ | | `assertCacheStatus`, `assertCacheDecision`, `parseCacheHeader`, `createCacheSink`, `filterCacheDecisions` | [`cache-prerender.md`](./cache-prerender.md) |
149
+ | | `assertShellStatus`, `parseShellStatus`, `shellCacheKey`, `SHELL_STATUS_HEADER`, `assertPprReplayStatus`, `parsePprReplayStatus`, `PPR_REPLAY_STATUS_HEADER` | [`cache-prerender.md`](./cache-prerender.md) |
150
+ | | `assertGeneratedRoutesMatch`, `diffGeneratedRoutes` | [`reverse-and-types.md`](./reverse-and-types.md) |
151
+ | `@rangojs/router/testing/dom` | `renderRoute` | [`client-components.md`](./client-components.md) |
152
+ | `@rangojs/router/testing/flight` | `renderToFlightString`, `normalizeFlight`, `assertFlightRuntimeAvailable` | [`flight.md`](./flight.md) |
153
+ | | `renderServerTree`, `findClientBoundaries`, `findElements`, `textContent`, `assertFlightTreeRuntimeAvailable` | [`server-tree.md`](./server-tree.md) |
154
+ | | `renderHandler` | [`render-handler.md`](./render-handler.md) |
155
+ | | `serveShellRequest`, `resetShellTestState` | [`serve-shell-request.md`](./serve-shell-request.md) |
156
+ | `@rangojs/router/testing/flight-matchers` | `flightMatchers` (`toMatchFlight`, `toMatchFlightSnapshot`) | [`flight.md`](./flight.md) |
157
+ | `@rangojs/router/testing/vitest` | `rangoTestConfig`, `rangoTestAliases`, `rangoInlineDeps`, `rangoUseClientTransform`, `rangoUseCacheTransform` | [`setup.md`](./setup.md) |
158
+ | `@rangojs/router/testing/e2e` | `createRangoE2E` and its standalone helpers; the cache/shell status helpers above are re-exported here | [`e2e-parity.md`](./e2e-parity.md) |
159
+
160
+ ## Pre-push checklist
161
+
162
+ Before pushing, run all of these and fix any failure (script names follow
163
+ [`./setup.md`](./setup.md); adjust to your `package.json`):
124
164
 
125
165
  1. `pnpm run typecheck` (or `pnpm exec tsc --noEmit`)
126
166
  2. `pnpm run test:unit` (node + DOM vitest)
127
167
  3. `pnpm run test:unit:rsc` (the react-server Flight project)
128
- 4. `pnpm run lint`
129
- 5. `pnpm run format`
168
+ 4. your lint and format scripts
169
+ 5. your Playwright suite (dev AND production projects)
130
170
 
131
171
  And: **every e2e has a production counterpart.** `parityDescribe` makes this
132
172
  automatic — if you wrote a plain `test.describe` for a behavior, convert it.