@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
@@ -2,7 +2,8 @@
2
2
 
3
3
  ## 4. Middleware / Route Protection
4
4
 
5
- React Router doesn't have built-in middleware. Protection is typically done in loaders:
5
+ React Router v6 and Remix v2 have no middleware, so protection is typically done
6
+ in loaders (v7 route `middleware` exports map to the same Rango shape below):
6
7
 
7
8
  ```typescript
8
9
  // React Router: auth check in loader
@@ -13,9 +14,27 @@ export async function loader({ request }) {
13
14
  }
14
15
 
15
16
  // Rango: router.use() for request-level auth
17
+ import {
18
+ createRouter,
19
+ cookies,
20
+ redirect,
21
+ type Middleware,
22
+ } from "@rangojs/router";
23
+
24
+ const authInit: Middleware = async (ctx, next) => {
25
+ const session = cookies().get("session")?.value;
26
+ if (session) ctx.set("user", await getUserFromSession(session));
27
+ await next();
28
+ };
29
+
30
+ const requireAuth: Middleware = async (ctx, next) => {
31
+ if (!ctx.get("user")) return redirect("/login");
32
+ await next();
33
+ };
34
+
16
35
  const router = createRouter({})
17
36
  .use(authInit) // all routes — resolves session
18
- .use("/dashboard/*", requireAuth) // scoped guard
37
+ .use("/dashboard/*", requireAuth) // /dashboard and everything under it
19
38
  .routes(urlpatterns);
20
39
  ```
21
40
 
@@ -122,7 +141,7 @@ export function meta() {
122
141
  }
123
142
 
124
143
  // Rango: Meta handle in server handlers
125
- import { Meta } from "@rangojs/router";
144
+ import { Meta, type Handler } from "@rangojs/router";
126
145
 
127
146
  const HomePage: Handler<"home"> = (ctx) => {
128
147
  const meta = ctx.use(Meta);
@@ -135,18 +154,20 @@ const HomePage: Handler<"home"> = (ctx) => {
135
154
  RR's data-derived `meta({ data })` maps to the same push from the LOADER that
136
155
  owns the data — `ctx.use(Meta)({ title: data.name })` in the loader body, with
137
156
  `loader(Def, { ssr: false })` when the title must be in the SSR'd
138
- head. See `/loader` → "Writing Handles from Loaders".
157
+ head. See `/loader` → "Writing Handles from Loaders". The loader flag is
158
+ unrelated to RR7's `ssr: false` (SPA mode), which has no route-wide Rango
159
+ equivalent: it makes the server settle that loader before the first flush.
139
160
 
140
- Add `<MetaTags />` in the Document component's `<head>`:
161
+ Add `<Html.Meta />` in the Document component's `<head>`:
141
162
 
142
163
  ```typescript
143
- import { MetaTags } from "@rangojs/router/client";
164
+ import { Html } from "@rangojs/router/client";
144
165
 
145
166
  function Document({ children }: { children: ReactNode }) {
146
167
  return (
147
168
  <html>
148
169
  <head>
149
- <MetaTags />
170
+ <Html.Meta />
150
171
  </head>
151
172
  <body>{children}</body>
152
173
  </html>
@@ -20,6 +20,8 @@ function ProductPage() {
20
20
  }
21
21
 
22
22
  // Rango: handler fetches and renders directly
23
+ import type { Handler } from "@rangojs/router";
24
+
23
25
  const ProductPage: Handler<"product"> = async (ctx) => {
24
26
  const product = await getProduct(ctx.params.slug);
25
27
  return <div>{product.name}</div>;
@@ -46,7 +48,9 @@ Use it only when you need capabilities beyond what the handler provides:
46
48
  document loads, never an HTTP 302 — pre-stream 302s move to middleware)
47
49
  - **`meta({ data })` / `handle` exports** — data-derived page metadata becomes
48
50
  a handle push from the loader body (`ctx.use(Meta)({ title: data.name })`),
49
- with `loader(L, { ssr: false })` when it must be in the SSR'd head
51
+ with `loader(L, { ssr: false })` when it must be in the SSR'd head (not
52
+ RR7's `ssr: false` SPA mode, which has no route-wide Rango equivalent: the
53
+ loader flag makes the server settle that loader before the first flush)
50
54
 
51
55
  If the React Router loader just fetches data for its page component AND the
52
56
  component can become a server component, merge it into the handler. If the
@@ -76,6 +80,7 @@ function EditProfile() {
76
80
  }
77
81
 
78
82
  // Rango: "use server" action + native form or useActionState
83
+ // actions/profile.ts
79
84
  "use server";
80
85
  import { redirect } from "@rangojs/router";
81
86
 
@@ -84,7 +89,10 @@ export async function updateProfile(formData: FormData): Promise<void> {
84
89
  throw redirect("/profile");
85
90
  }
86
91
 
87
- // Client component:
92
+ // EditProfile.tsx — a server or client component; passing the action itself
93
+ // (not a wrapper function) keeps the form working with JavaScript disabled.
94
+ import { updateProfile } from "./actions/profile";
95
+
88
96
  function EditProfile() {
89
97
  return (
90
98
  <form action={updateProfile}>
@@ -122,9 +130,12 @@ function ProductPrice() {
122
130
  }
123
131
  ```
124
132
 
125
- `useLoader()` provides live data that stays fresh — it re-fetches on navigation
126
- and after actions (controlled by `revalidate()`). This is different from
127
- `useLoaderData()` which just reads a snapshot.
133
+ `useLoader()` provides live data that stays fresh — the loader re-runs on
134
+ navigation and after actions (controlled by `revalidate()`), and the result also
135
+ exposes `isLoading`, `error`, `load`, and `refetch`. Register the loader on the
136
+ route (`loader(PriceLoader)`) so its data is in the tree the component reads;
137
+ the read suspends until it arrives, so keep a `<Suspense>` or `loading()` above
138
+ it. See `/loader` and `/hooks`.
128
139
 
129
140
  ### useActionData
130
141
 
@@ -155,8 +166,12 @@ function EditForm() {
155
166
  // Rango: useActionState (standard React hook)
156
167
  "use client";
157
168
  import { useActionState } from "react";
158
- import { saveForm } from "../actions"; // "use server" function
169
+ // "use server" function with the useActionState shape:
170
+ // export async function saveForm(prev: State, formData: FormData): Promise<State>
171
+ import { saveForm } from "../actions";
159
172
 
173
+ // Pass the imported action straight to useActionState — not an inline closure —
174
+ // so the form still posts and re-renders with JavaScript disabled.
160
175
  function EditForm() {
161
176
  const [state, action, pending] = useActionState(saveForm, null);
162
177
  return (
@@ -10,9 +10,13 @@ npm remove react-router @react-router/dev @react-router/node @react-router/serve
10
10
  # Library mode:
11
11
  npm remove react-router react-router-dom
12
12
 
13
- npm install @rangojs/router
13
+ npm install @rangojs/router # keep react and react-dom
14
14
  ```
15
15
 
16
+ `rango()` includes `@vitejs/plugin-rsc` and supplies the client and server
17
+ entries. Add `@vitejs/plugin-react` only if you want Fast Refresh or the React
18
+ Compiler.
19
+
16
20
  Replace the `@react-router/dev` Vite plugin with `rango()`:
17
21
 
18
22
  ```typescript
@@ -34,7 +38,7 @@ import { createRouter } from "@rangojs/router";
34
38
  import { Document } from "./document";
35
39
  import { urlpatterns } from "./urls";
36
40
 
37
- export default createRouter({
41
+ export const router = createRouter({
38
42
  document: Document,
39
43
  }).routes(urlpatterns);
40
44
  ```
@@ -58,7 +62,7 @@ loader → fetch in handler, or createLoader() — a Rango
58
62
  action → "use server" function
59
63
  meta → ctx.use(Meta) in handler; meta({ data }) →
60
64
  ctx.use(Meta) push in the loader that owns data
61
- headers → ctx.header() in handler or middleware
65
+ headers → ctx.headers.set() in a handler; ctx.header() in middleware
62
66
  shouldRevalidate → revalidate() DSL
63
67
  ErrorBoundary → errorBoundary() DSL
64
68
  HydrateFallback → loading() DSL
@@ -107,15 +111,15 @@ export function ErrorBoundary() {
107
111
 
108
112
  ```typescript
109
113
  // Rango: urls.tsx + handler
110
- import { notFound } from "@rangojs/router";
114
+ import { Meta, notFound, type Handler } from "@rangojs/router";
111
115
 
112
116
  const ProductPage: Handler<"product"> = async (ctx) => {
113
117
  const product = await getProduct(ctx.params.slug);
114
- if (!product) notFound("Product not found");
118
+ if (!product) notFound("Product not found"); // throws: nearest notFoundBoundary, else the router's notFound
115
119
 
116
120
  const meta = ctx.use(Meta);
117
121
  meta({ title: product.name });
118
- ctx.header("Cache-Control", "max-age=300");
122
+ ctx.headers.set("Cache-Control", "max-age=300"); // response headers
119
123
 
120
124
  return <div>{product.name}</div>;
121
125
  };
@@ -130,16 +134,21 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
130
134
 
131
135
  Key shift: the route module's scattered exports consolidate into the handler
132
136
  (data fetching, meta, headers) and the DSL (revalidation, error boundary, loading).
137
+ The `action` export becomes a `"use server"` function (see
138
+ [`./data-and-actions.md`](./data-and-actions.md)).
133
139
 
134
140
  The loader-shaped variant is equally valid — and closer to the RR module when
135
141
  the loader carried authority. A `createLoader()` body can throw `notFound()`
136
142
  for the missing product AND push the data-derived meta itself
137
143
  (`ctx.use(Meta)({ title: product.name })`); register it with
138
144
  `loader(ProductLoader, { ssr: false })` when the 404 status and
139
- title must be in the document deterministically. See `/loader` → "Loader
140
- Authority" and "Writing Handles from Loaders". (One RR habit that does NOT
141
- carry over: a loader `throw redirect()` is a client-side navigate on document
142
- loads, never an HTTP 302 — pre-stream 302s belong in middleware.)
145
+ title must be in the document deterministically (the flag makes the server
146
+ settle that loader before the first flush; it is unrelated to RR7's
147
+ `ssr: false` SPA mode, which has no route-wide Rango equivalent). See
148
+ `/loader` → "Loader Authority" and "Writing Handles from Loaders". (One RR
149
+ habit that does NOT carry over: a loader `throw redirect()` is a client-side
150
+ navigate on document loads, never an HTTP 302 — pre-stream 302s belong in
151
+ middleware.)
143
152
 
144
153
  ### RR7 file routing → urls() DSL
145
154
 
@@ -15,12 +15,14 @@ See also: `/response-routes` for the base response route API (path.json, path.te
15
15
  ## Defining Negotiated Routes
16
16
 
17
17
  Declare the same URL pattern with both an RSC route and one or more response-type routes.
18
- Order within the `urls()` array does not matter — the trie merges them at build time.
18
+ The trie merges them into one negotiated leaf at build time, whatever their order. Order
19
+ only decides the default for `*/*`, a missing `Accept`, or an `Accept` no variant serves:
20
+ the first-defined route wins (rule 4 below).
19
21
 
20
22
  ```typescript
21
23
  import { urls } from "@rangojs/router";
22
24
 
23
- export const urlpatterns = urls(({ path, layout, include }) => [
25
+ export const urlpatterns = urls(({ path }) => [
24
26
  // RSC page + JSON API on the same URL
25
27
  path("/products/:id", ProductPage, { name: "product" }),
26
28
  path.json(
@@ -38,10 +40,10 @@ When an API client requests the same URL (`Accept: application/json`), the JSON
38
40
 
39
41
  ## Negotiation Rules
40
42
 
41
- 1. **Q-value priority** — higher `q` wins (`Accept: application/json;q=0.9, text/html;q=1.0` serves RSC)
43
+ 1. **Q-value priority** — higher `q` wins (`Accept: application/json;q=0.9, text/html;q=1.0` serves RSC); `q=0` entries are ignored
42
44
  2. **Client order tiebreaker** — when q-values are equal, the type listed first in Accept wins (matches Express/Hono behavior)
43
- 3. **Specific MIME match** — the variant whose MIME type appears in Accept wins
44
- 4. **Wildcard / empty Accept** — `*/*` and missing Accept fall back to route definition order (the first-defined variant wins); when the RSC route wins this way, it serves the HTML document
45
+ 3. **Specific MIME match** — the variant whose MIME type appears in Accept wins; a type wildcard such as `text/*` picks the first candidate of that type
46
+ 4. **Wildcard / empty / unmatched Accept** — `*/*`, a missing Accept, and an Accept that no candidate serves fall back to route definition order (the first-defined variant wins); when the RSC route wins this way, it serves the HTML document
45
47
  5. **All responses** on a negotiated URL get `Vary: Accept` header, including the RSC side
46
48
 
47
49
  RSC participates as a candidate alongside response-type variants under two MIME
@@ -150,9 +152,12 @@ no payloads, so response types resolve to `never`.
150
152
  1. **Build time**: `buildRouteTrie()` calls `mergeLeaves()` when multiple routes share a pattern.
151
153
  RSC routes become the primary trie leaf; response-type routes are stored in the `nv`
152
154
  (negotiate variants) array on the leaf. The `rf` (rsc-first) flag tracks definition order.
153
- 2. **Runtime**: `previewRoute()` reads `negotiateVariants` from the trie match result.
155
+ 2. **Runtime**: `negotiateRoute()` (called during request classification) reads `negotiateVariants` from the trie match result.
154
156
  It parses the `Accept` header (extracting q-values and order), builds a candidate list
155
- (RSC as `text/html` + response-type variants), and calls `pickNegotiateVariant()`.
157
+ (the response-type variants in definition order, plus RSC as `text/html` and `text/x-component` —
158
+ placed first when the RSC route was defined first, otherwise last),
159
+ and calls `pickNegotiateVariant()`. When a variant with different param names wins
160
+ (`/widgets/:id` vs `/widgets/:file`), params are re-keyed to the variant's names.
156
161
  3. **Candidate matching**: walks the client's sorted Accept list (by q desc, then order asc),
157
162
  matching each entry against candidates. Wildcards (`*/*`, `text/*`) fall back to definition order.
158
163
  4. **Vary header**: both the response-route handler wrapper and the RSC handler wrapper
@@ -9,15 +9,17 @@ argument-hint:
9
9
  Use this when you need to understand request latency, cache decisions,
10
10
  revalidation behavior, loader overlap, or production traces.
11
11
 
12
- Rango exposes two complementary observability surfaces:
12
+ Rango exposes three complementary observability surfaces:
13
13
 
14
14
  1. **Performance timeline** (`debugPerformance`) — per-request waterfall for
15
15
  local or targeted debugging. It prints to the console and emits
16
16
  `Server-Timing`.
17
- 2. **Structured telemetry** (`telemetry`) — lifecycle events sent to a pluggable
18
- sink for production monitoring, OpenTelemetry, or custom metrics.
17
+ 2. **Structured telemetry** (`telemetry`) — discrete lifecycle events sent to a
18
+ pluggable sink for production monitoring, OpenTelemetry, or custom metrics.
19
+ 3. **Tracing** (`tracing`) — the same phases as the timeline, emitted as spans
20
+ into OpenTelemetry, Cloudflare Workers, or Vercel traces.
19
21
 
20
- The essentials are below. The exported `TelemetryEvent` union type
22
+ All three are off by default. The essentials are below. The exported `TelemetryEvent` union type
21
23
  (`import type { TelemetryEvent } from "@rangojs/router"`) is the full event
22
24
  contract — every event kind and its fields are typed there.
23
25
 
@@ -35,7 +37,8 @@ const router = createRouter({
35
37
  });
36
38
  ```
37
39
 
38
- Or enable for selected requests from middleware:
40
+ Or enable for selected requests from middleware (route `middleware()` inside
41
+ `urls()`, or `router.use()` for every request):
39
42
 
40
43
  ```typescript
41
44
  middleware(async (ctx, next) => {
@@ -52,11 +55,83 @@ shared-axis waterfall and adds a `Server-Timing` header.
52
55
  Read the timeline as intervals:
53
56
 
54
57
  - `handler:total` is the whole router request.
55
- - `render:total` / `ssr-render-html` show the render pass.
56
- - `loader:*` rows should overlap render work. If a loader starts only after the
57
- render bar, it is serialized latency.
58
- - Cache, route matching, middleware pre/post, RSC serialization, and SSR phases
59
- appear as separate spans, so the slow phase is visible without guessing.
58
+ - `render:total:<routeName>` (or `render:total` for unnamed routes) and
59
+ `ssr:render-html` show the render pass.
60
+ - `loader:<id>` rows should overlap render work. If a loader starts only after
61
+ the render bar, it is serialized latency.
62
+ - Route matching (`match:*`), middleware (`middleware:<name>:pre` / `:post`),
63
+ actions (`action:<id>`), cache, RSC serialization, and SSR setup appear as
64
+ separate rows, so the slow phase is visible without guessing.
65
+
66
+ ### Reading a PPR shell HIT
67
+
68
+ A `ppr` route serving from its shell cache commits the response as soon as the
69
+ stored prelude is read, so its waterfall ends at `ppr:shell-commit`; what runs
70
+ after the commit (the capture snapshot, the seeded tail, the resumed holes)
71
+ prints as one `shell tail` line when it finishes, and rides the NEXT request's
72
+ `Server-Timing` as `ppr-tail`. The store-read rows under `ppr:shell-read` come
73
+ from the built-in stores (`CFCacheStore`'s prelude-first read shown here;
74
+ `VercelCacheStore` reports `hit store`, one `ppr:shell-match (store)` row, and
75
+ a `serial` marker read); other stores show `ppr:shell-read`,
76
+ `ppr:shell-open`, and `ppr:shell-commit` only. From `vite preview` of
77
+ `tests/cloudflare-basic` `/ppr-large/holes` (614 KB prelude, 1.0 MB capture
78
+ snapshot), with the timeline column dropped:
79
+
80
+ ```
81
+ [RSC Perf] GET /ppr-large/holes (4.00ms)
82
+ start dur span
83
+ 1.00ms 0.00ms middleware:*#2
84
+ 1.00ms 3.00ms ppr:shell-read (hit l1)
85
+ 1.00ms 1.00ms ppr:shell-match (l1)
86
+ 1.00ms 3.00ms render:total:pprLargeHoles
87
+ 2.00ms 0.00ms ppr:shell-head (bytes=27943)
88
+ 2.00ms 2.00ms ppr:shell-prelude (bytes=628949)
89
+ 2.00ms 0.00ms ppr:shell-marker (tags=0 parallel commit-wait=0.00ms)
90
+ 4.00ms 0.00ms ppr:shell-open (cpu raw prelude=628949b)
91
+ 4.00ms 0.00ms ppr:shell-commit (cpu chunks=20 prelude=628949b)
92
+ 0.00ms 4.00ms handler:total
93
+ ```
94
+
95
+ and, when the tail finishes, one console line (wrapped here):
96
+
97
+ ```
98
+ [RSC Perf] GET /ppr-large/holes shell tail: complete snapshot=4ms
99
+ snapshot-read=2ms snapshot-bytes=1019145b snapshot-parse-cpu=2ms
100
+ records=segment:1 pruned=item:5 seed=4ms seed-cpu=0ms match=4ms handover=4ms
101
+ first-html=5ms complete=55ms prelude=628949b tail=1033762b
102
+ ```
103
+
104
+ - `ppr:shell-match` then `head` then `prelude`: the store read. Only the head
105
+ and the prelude are read before the first byte; the snapshot is read off the
106
+ commit path (`snapshot=` in the tail line). A KV hit after a Cache API miss
107
+ starts with a `ppr:shell-l1-miss` row (the L1 attempt and why it missed). A
108
+ `ppr:shell-memo (hit …)` row means the shell came from the isolate's memo:
109
+ no store read, only the marker check.
110
+ - `ppr:shell-marker` is the tag-marker check, on every `CFCacheStore` read; it
111
+ runs alongside `ppr:shell-prelude`, and `commit-wait` is how much it held the
112
+ commit back (with `tags=0` it resolves at once). For a tagged shell it adds
113
+ `memo=` (how the per-isolate marker memo answered: `fresh`, `stale` served
114
+ while it refreshes, `read` from the store, `bypass` for a request carrying
115
+ the fresh-reads cookie), `hint=<shell tags hinted>/<tags hinted>` with `lead=` (how long the
116
+ hinted marker reads ran before the entry named its tags), and `fresh-reads`
117
+ when the request carried the cookie.
118
+ - `records=` counts the snapshot records the tail was seeded with, by
119
+ family; `pruned=` counts the ones the capture dropped because no reader of
120
+ the entry consumes them. Here every HIT tail replays the handler layer from
121
+ the one segment record, so the five `"use cache"` item records that produced
122
+ it were not stored.
123
+ - `snapshot=`, `seed=`, `match=`, `first-html=` are offsets from the commit,
124
+ not durations; `snapshot-read=`, `snapshot-parse-cpu=` and `seed-cpu=` are
125
+ durations.
126
+ - On a deployed worker the clock only moves on I/O, so the CPU-only numbers
127
+ (`ppr:shell-open`, `ppr:shell-commit`, `snapshot-parse-cpu`, `seed-cpu`)
128
+ read 0 there; the byte counts are the cost to watch.
129
+
130
+ The console waterfall uses these labels as written. In the `Server-Timing`
131
+ header, colons become hyphens and other non-alphanumeric characters are
132
+ dropped (`ssr:render-html` → `ssr-render-html`, `handler:total` →
133
+ `handler-total`), and nested rows get a `d<depth>-` prefix. The full metric
134
+ list is in the repository's `packages/rangojs-router/docs/telemetry.md`.
60
135
 
61
136
  **Deployed Cloudflare caveat**: on production Workers, timers are frozen
62
137
  during request execution (Spectre mitigation), so `Server-Timing` durations
@@ -106,7 +181,9 @@ const router = createRouter({
106
181
  On **Cloudflare Workers**, use `createCloudflareTracing` for the `tracing` slot
107
182
  instead — it emits the same phases as native Cloudflare custom spans (in the
108
183
  Workers trace waterfall, next to the automatic KV/D1/fetch spans), with no
109
- `@opentelemetry/api` dependency:
184
+ `@opentelemetry/api` dependency. It reads the tracer from the `ctx` your Worker
185
+ passes to `router.fetch(request, { env, ctx })`, and is a pass-through when
186
+ Workers tracing is not enabled for the Worker:
110
187
 
111
188
  ```typescript
112
189
  import { createRouter } from "@rangojs/router";
@@ -226,6 +303,7 @@ Then inspect:
226
303
 
227
304
  ## Zero-overhead defaults
228
305
 
229
- `debugPerformance` is off by default, and `telemetry` emits nothing unless a sink
230
- is configured. Per-request `ctx.debugPerformance()` lets you turn on the
231
- waterfall only for the route, user, or query param you are investigating.
306
+ `debugPerformance` is off by default, `telemetry` emits nothing unless a sink
307
+ is configured, and with `tracing` unset every span call runs the work directly.
308
+ Per-request `ctx.debugPerformance()` lets you turn on the waterfall only for the
309
+ route, user, or query param you are investigating.