@rangojs/router 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +254 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  6. package/dist/types/browser/rsc-router.d.ts +1 -1
  7. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  8. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  9. package/dist/types/cache/cache-scope.d.ts +34 -5
  10. package/dist/types/cache/handle-capture.d.ts +41 -3
  11. package/dist/types/cache/handle-snapshot.d.ts +53 -13
  12. package/dist/types/cache/segment-codec.d.ts +10 -3
  13. package/dist/types/cache/taint.d.ts +1 -1
  14. package/dist/types/cache/types.d.ts +15 -3
  15. package/dist/types/client.d.ts +4 -4
  16. package/dist/types/client.rsc.d.ts +4 -5
  17. package/dist/types/components/DefaultDocument.d.ts +5 -2
  18. package/dist/types/context-var.d.ts +6 -5
  19. package/dist/types/escape-script.d.ts +1 -1
  20. package/dist/types/handles/MetaTags.d.ts +5 -4
  21. package/dist/types/handles/Scripts.d.ts +8 -7
  22. package/dist/types/handles/meta.d.ts +2 -2
  23. package/dist/types/handles/script.d.ts +17 -16
  24. package/dist/types/href-client.d.ts +1 -1
  25. package/dist/types/html.d.ts +12 -0
  26. package/dist/types/index.d.ts +1 -1
  27. package/dist/types/index.rsc.d.ts +3 -2
  28. package/dist/types/loader-store.d.ts +7 -4
  29. package/dist/types/reverse.d.ts +36 -4
  30. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  31. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +1 -1
  34. package/dist/types/router/debug-manifest.d.ts +3 -2
  35. package/dist/types/router/error-handling.d.ts +20 -0
  36. package/dist/types/router/handler-context.d.ts +6 -0
  37. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  38. package/dist/types/router/middleware-types.d.ts +3 -2
  39. package/dist/types/router/router-context.d.ts +3 -1
  40. package/dist/types/router/router-interfaces.d.ts +24 -2
  41. package/dist/types/router/router-options.d.ts +8 -5
  42. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  43. package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
  44. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  45. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
  46. package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
  47. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  48. package/dist/types/router/segment-wrappers.d.ts +1 -1
  49. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  50. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  51. package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
  52. package/dist/types/rsc/shell-capture.d.ts +5 -4
  53. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  54. package/dist/types/rsc/types.d.ts +10 -0
  55. package/dist/types/server/context.d.ts +33 -3
  56. package/dist/types/server/handle-store.d.ts +26 -2
  57. package/dist/types/server/request-context.d.ts +24 -21
  58. package/dist/types/ssr/index.d.ts +15 -0
  59. package/dist/types/static-handler.d.ts +4 -1
  60. package/dist/types/testing/generated-routes.d.ts +2 -2
  61. package/dist/types/testing/internal/context.d.ts +1 -1
  62. package/dist/types/testing/render-route.d.ts +32 -6
  63. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  64. package/dist/types/testing/vitest.d.ts +35 -1
  65. package/dist/types/theme/ThemeScript.d.ts +3 -3
  66. package/dist/types/types/cache-types.d.ts +14 -10
  67. package/dist/types/types/handler-context.d.ts +14 -0
  68. package/dist/types/types/loader-types.d.ts +8 -6
  69. package/dist/types/urls/response-types.d.ts +3 -12
  70. package/dist/types/use-loader.d.ts +12 -3
  71. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  72. package/dist/types/vite/rango.d.ts +1 -1
  73. package/dist/vite/index.js +1381 -1346
  74. package/package.json +2 -2
  75. package/skills/api-client/SKILL.md +22 -10
  76. package/skills/breadcrumbs/SKILL.md +65 -22
  77. package/skills/bundle-analysis/SKILL.md +22 -22
  78. package/skills/cache-guide/SKILL.md +207 -97
  79. package/skills/caching/SKILL.md +295 -93
  80. package/skills/catalog.json +16 -16
  81. package/skills/client-urls/SKILL.md +53 -31
  82. package/skills/cloudflare/SKILL.md +72 -20
  83. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  84. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  85. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  86. package/skills/comparison/SKILL.md +5 -0
  87. package/skills/comparison/references/framework-comparison.md +39 -33
  88. package/skills/composability/SKILL.md +38 -29
  89. package/skills/css/SKILL.md +10 -13
  90. package/skills/debug-manifest/SKILL.md +74 -31
  91. package/skills/defer-hydration/SKILL.md +2 -1
  92. package/skills/deployment-caching/SKILL.md +6 -1
  93. package/skills/document-cache/SKILL.md +92 -28
  94. package/skills/fonts/SKILL.md +16 -8
  95. package/skills/handler-use/SKILL.md +53 -17
  96. package/skills/hooks/SKILL.md +28 -20
  97. package/skills/hooks/data.md +23 -3
  98. package/skills/hooks/handle-and-actions.md +31 -16
  99. package/skills/hooks/navigation.md +39 -10
  100. package/skills/hooks/outlets.md +13 -6
  101. package/skills/hooks/state.md +37 -15
  102. package/skills/hooks/urls.md +7 -5
  103. package/skills/host-router/SKILL.md +64 -12
  104. package/skills/i18n/SKILL.md +60 -34
  105. package/skills/intercept/SKILL.md +161 -26
  106. package/skills/layout/SKILL.md +106 -50
  107. package/skills/links/SKILL.md +51 -19
  108. package/skills/loader/SKILL.md +242 -106
  109. package/skills/middleware/SKILL.md +119 -23
  110. package/skills/migrate-nextjs/SKILL.md +100 -45
  111. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  112. package/skills/migrate-react-router/SKILL.md +49 -38
  113. package/skills/migrate-react-router/component-migration.md +25 -6
  114. package/skills/migrate-react-router/data-and-actions.md +18 -5
  115. package/skills/migrate-react-router/route-mapping.md +12 -6
  116. package/skills/mime-routes/SKILL.md +12 -7
  117. package/skills/observability/SKILL.md +28 -14
  118. package/skills/parallel/SKILL.md +75 -39
  119. package/skills/ppr/SKILL.md +345 -150
  120. package/skills/prerender/SKILL.md +189 -100
  121. package/skills/rango/SKILL.md +65 -34
  122. package/skills/react-compiler/SKILL.md +6 -5
  123. package/skills/response-routes/SKILL.md +133 -25
  124. package/skills/route/SKILL.md +165 -60
  125. package/skills/router-setup/SKILL.md +418 -240
  126. package/skills/scripts/SKILL.md +31 -20
  127. package/skills/server-actions/SKILL.md +220 -59
  128. package/skills/shell-manifest/SKILL.md +12 -7
  129. package/skills/streams-and-websockets/SKILL.md +32 -14
  130. package/skills/tailwind/SKILL.md +37 -33
  131. package/skills/testing/SKILL.md +68 -32
  132. package/skills/testing/cache-prerender.md +55 -29
  133. package/skills/testing/client-components.md +59 -6
  134. package/skills/testing/e2e-parity.md +12 -2
  135. package/skills/testing/flight.md +19 -11
  136. package/skills/testing/handles.md +8 -5
  137. package/skills/testing/loader.md +8 -5
  138. package/skills/testing/middleware.md +8 -3
  139. package/skills/testing/predicates.md +120 -0
  140. package/skills/testing/render-handler.md +70 -22
  141. package/skills/testing/response-routes.md +11 -4
  142. package/skills/testing/reverse-and-types.md +20 -14
  143. package/skills/testing/server-actions.md +12 -3
  144. package/skills/testing/server-tree.md +9 -0
  145. package/skills/testing/setup.md +9 -5
  146. package/skills/theme/SKILL.md +106 -29
  147. package/skills/typesafety/SKILL.md +20 -9
  148. package/skills/typesafety/env-and-bindings.md +126 -47
  149. package/skills/typesafety/generated-files-and-cli.md +26 -14
  150. package/skills/typesafety/params-and-search.md +44 -19
  151. package/skills/typesafety/route-types.md +50 -14
  152. package/skills/use-cache/SKILL.md +200 -82
  153. package/skills/vercel/SKILL.md +21 -11
  154. package/skills/view-transitions/SKILL.md +41 -20
  155. package/src/__augment-tests__/augmented.check.ts +26 -1
  156. package/src/browser/index.ts +2 -2
  157. package/src/browser/navigation-bridge.ts +3 -4
  158. package/src/browser/navigation-transaction.ts +22 -2
  159. package/src/browser/partial-update.ts +35 -25
  160. package/src/browser/react/ScrollRestoration.tsx +6 -6
  161. package/src/browser/rsc-router.tsx +1 -1
  162. package/src/browser/scroll-restoration.ts +53 -6
  163. package/src/cache/cache-exec-scope.ts +37 -4
  164. package/src/cache/cache-key-utils.ts +1 -6
  165. package/src/cache/cache-runtime.ts +230 -61
  166. package/src/cache/cache-scope.ts +76 -12
  167. package/src/cache/document-cache.ts +15 -0
  168. package/src/cache/handle-capture.ts +90 -10
  169. package/src/cache/handle-snapshot.ts +132 -28
  170. package/src/cache/segment-codec.ts +26 -7
  171. package/src/cache/tag-invalidation.ts +4 -3
  172. package/src/cache/taint.ts +1 -1
  173. package/src/cache/types.ts +16 -3
  174. package/src/client.rsc.tsx +10 -9
  175. package/src/client.tsx +5 -4
  176. package/src/components/DefaultDocument.tsx +7 -2
  177. package/src/context-var.ts +6 -5
  178. package/src/escape-script.ts +1 -1
  179. package/src/handles/MetaTags.tsx +13 -11
  180. package/src/handles/Scripts.tsx +11 -10
  181. package/src/handles/meta.ts +2 -2
  182. package/src/handles/script.ts +17 -16
  183. package/src/href-client.ts +1 -1
  184. package/src/html.ts +12 -0
  185. package/src/index.rsc.ts +5 -0
  186. package/src/index.ts +1 -0
  187. package/src/loader-store.ts +7 -4
  188. package/src/prerender/build-shell-capture.ts +8 -11
  189. package/src/reverse.ts +44 -4
  190. package/src/route-definition/dsl-helpers.ts +117 -30
  191. package/src/route-definition/helpers-types.ts +33 -9
  192. package/src/route-definition/resolve-handler-use.ts +121 -6
  193. package/src/route-types.ts +0 -3
  194. package/src/router/debug-manifest.ts +10 -2
  195. package/src/router/error-handling.ts +51 -15
  196. package/src/router/handler-context.ts +34 -25
  197. package/src/router/intercept-resolution.ts +125 -39
  198. package/src/router/loader-resolution.ts +56 -11
  199. package/src/router/match-api.ts +12 -17
  200. package/src/router/match-middleware/background-revalidation.ts +161 -96
  201. package/src/router/match-middleware/cache-lookup.ts +93 -10
  202. package/src/router/match-middleware/cache-store.ts +17 -90
  203. package/src/router/middleware-types.ts +3 -5
  204. package/src/router/middleware.ts +52 -9
  205. package/src/router/prerender-match.ts +53 -19
  206. package/src/router/route-snapshot.ts +11 -1
  207. package/src/router/router-context.ts +2 -0
  208. package/src/router/router-interfaces.ts +25 -2
  209. package/src/router/router-options.ts +8 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -12
  211. package/src/router/segment-resolution/loader-cache.ts +205 -45
  212. package/src/router/segment-resolution/loader-mask.ts +12 -22
  213. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  214. package/src/router/segment-resolution/mask-nested.ts +7 -7
  215. package/src/router/segment-resolution/revalidation.ts +34 -6
  216. package/src/router/segment-wrappers.ts +4 -0
  217. package/src/rsc/render-pipeline.ts +3 -0
  218. package/src/rsc/response-route-handler.ts +11 -3
  219. package/src/rsc/rsc-rendering.ts +8 -45
  220. package/src/rsc/shell-capture-constants.ts +13 -0
  221. package/src/rsc/shell-capture.ts +111 -90
  222. package/src/rsc/ssr-setup.ts +14 -8
  223. package/src/rsc/types.ts +11 -0
  224. package/src/server/context.ts +88 -11
  225. package/src/server/cookie-store.ts +5 -4
  226. package/src/server/handle-store.ts +232 -7
  227. package/src/server/request-context.ts +35 -30
  228. package/src/ssr/index.tsx +40 -7
  229. package/src/static-handler.ts +4 -1
  230. package/src/testing/flight-runtime.d.ts +21 -0
  231. package/src/testing/generated-routes.ts +2 -2
  232. package/src/testing/internal/context.ts +1 -1
  233. package/src/testing/internal/flight-client-globals.ts +7 -5
  234. package/src/testing/render-route.tsx +85 -24
  235. package/src/testing/run-loader.ts +5 -1
  236. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  237. package/src/testing/vitest.ts +62 -1
  238. package/src/theme/ThemeScript.tsx +3 -3
  239. package/src/types/cache-types.ts +14 -10
  240. package/src/types/handler-context.ts +14 -0
  241. package/src/types/loader-types.ts +8 -6
  242. package/src/urls/response-types.ts +3 -23
  243. package/src/use-loader.tsx +12 -3
  244. package/src/vite/plugins/use-cache-transform.ts +118 -74
  245. package/src/vite/rango.ts +7 -3
package/README.md CHANGED
@@ -8,11 +8,19 @@ everything past the core is opt-in.
8
8
  > releases and are noted in the changelog. npm `latest` is the current 0.x
9
9
  > release; the `experimental` tag tracks `main` between tagged releases.
10
10
 
11
- This page is a tour: it builds one small shop and meets the entire core API
12
- along the way — about six primitives. Everything else is opt-in and linked at
13
- the end. For the design rationale behind these APIs, read
14
- [Why Rango](https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/why-rango.md); this page shows how it feels, that page
15
- argues why it's right.
11
+ This page is a tour. It builds one small shop in six steps and covers the
12
+ whole core API along the way:
13
+
14
+ 1. **Pages**: nested layouts, named routes, typed params
15
+ 2. **Data**: fetching in handlers, and loaders for data that stays live
16
+ 3. **Mutations**: Server Actions that work without JavaScript
17
+ 4. **Speed**: cached pages with live loaders, prefetching, prerendering
18
+ 5. **APIs**: JSON and other responses served from the same URLs as pages
19
+ 6. **Tests**: unit tests for loaders, e2e tests in dev and production
20
+
21
+ Everything else is opt-in and linked at the end. This page shows how the API
22
+ feels to use. For why it's designed this way, read
23
+ [Why Rango](https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/why-rango.md).
16
24
 
17
25
  ## Start a new app
18
26
 
@@ -36,25 +44,46 @@ or newer.
36
44
  If you are adding Rango to an existing Vite RSC project:
37
45
 
38
46
  ```bash
39
- npm install @rangojs/router@latest react @vitejs/plugin-rsc
47
+ npm install @rangojs/router@latest react react-dom
48
+ npm install -D vite
40
49
  ```
41
50
 
42
51
  ```ts
43
52
  // vite.config.ts
44
- import react from "@vitejs/plugin-react";
45
53
  import { defineConfig } from "vite";
46
54
  import { rango } from "@rangojs/router/vite";
47
55
 
48
56
  export default defineConfig({
49
- plugins: [react(), rango({ preset: "cloudflare" })],
57
+ plugins: [rango()],
50
58
  });
51
59
  ```
52
60
 
53
- The `cloudflare` preset targets Cloudflare Workers (add
54
- `@cloudflare/vite-plugin`); the `vercel` preset emits a ready-to-deploy
55
- `.vercel/output` (Build Output API) from a plain `vite build` — see the
56
- [`/vercel` skill](./skills/vercel/SKILL.md); omit `preset` for the default
57
- Node setup.
61
+ That is the whole Node setup. `rango()` includes `@vitejs/plugin-rsc` and
62
+ supplies the client and server entries, so you don't need an `index.html` or
63
+ entry files. It finds the router by looking for the `createRouter()` call in
64
+ your source. Add `@vitejs/plugin-react` only if you want Fast Refresh or the
65
+ React Compiler.
66
+
67
+ For Cloudflare Workers, use the `cloudflare` preset together with
68
+ `@cloudflare/vite-plugin`:
69
+
70
+ ```ts
71
+ import { cloudflare } from "@cloudflare/vite-plugin";
72
+
73
+ export default defineConfig({
74
+ plugins: [
75
+ rango({ preset: "cloudflare" }),
76
+ cloudflare({
77
+ viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] },
78
+ }),
79
+ ],
80
+ });
81
+ ```
82
+
83
+ The `vercel` preset turns a plain `vite build` into a ready-to-deploy
84
+ `.vercel/output` (Build Output API). See the
85
+ [`/cloudflare`](./skills/cloudflare/SKILL.md) and
86
+ [`/vercel`](./skills/vercel/SKILL.md) skills.
58
87
 
59
88
  ## Using the skills with your coding agent
60
89
 
@@ -75,60 +104,98 @@ is at `skills/catalog.json`.
75
104
 
76
105
  ## 1. Pages
77
106
 
78
- A router is a tree. `path()` places a page, `layout()` wraps children,
79
- `{ name }` gives a route an identity:
107
+ A router is a tree. `path()` places a page, `layout()` wraps its children,
108
+ and `{ name }` gives a route an identity. Layouts nest, so each part of the
109
+ site wraps only what it owns:
80
110
 
81
111
  ```tsx
82
112
  // src/router.tsx
83
113
  import { createRouter, urls } from "@rangojs/router";
84
114
  import { Document } from "./document";
115
+ import { SiteLayout } from "./layouts/site";
85
116
  import { ShopLayout } from "./layouts/shop";
86
117
  import { HomePage } from "./routes/home";
87
- import { ProductPage } from "./routes/product";
118
+ import { ProductList, ProductPage } from "./routes/product";
88
119
 
89
120
  const urlpatterns = urls(({ path, layout }) => [
90
- layout(<ShopLayout />, () => [
121
+ layout(<SiteLayout />, () => [
91
122
  path("/", HomePage, { name: "home" }),
92
- path("/products/:slug", ProductPage, { name: "product" }),
123
+
124
+ layout(<ShopLayout />, () => [
125
+ path("/products", ProductList, { name: "products" }),
126
+ path("/products/:slug", ProductPage, { name: "product" }),
127
+ ]),
93
128
  ]),
94
129
  ]);
95
130
 
96
131
  export const router = createRouter({ document: Document }).routes(urlpatterns);
97
132
  ```
98
133
 
134
+ A layout renders its children through `<Outlet />`. `SiteLayout` wraps every
135
+ page, and `ShopLayout` adds the category sidebar only to the product routes:
136
+
137
+ ```tsx
138
+ // src/layouts/site.tsx
139
+ import { Link, Outlet } from "@rangojs/router/client";
140
+
141
+ export function SiteLayout() {
142
+ return (
143
+ <>
144
+ <header>
145
+ <Link to="/">Home</Link> <Link to="/products">Shop</Link>
146
+ </header>
147
+ <Outlet /> {/* the home page or ShopLayout */}
148
+ </>
149
+ );
150
+ }
151
+ ```
152
+
99
153
  ```tsx
100
154
  // src/layouts/shop.tsx
101
155
  import { Outlet } from "@rangojs/router/client";
102
156
 
103
157
  export function ShopLayout() {
104
158
  return (
105
- <div>
106
- <nav>Shop</nav>
159
+ <div className="shop">
160
+ <CategorySidebar />
107
161
  <main>
108
- <Outlet /> {/* child routes render here */}
162
+ <Outlet /> {/* the product list or a product page */}
109
163
  </main>
110
164
  </div>
111
165
  );
112
166
  }
113
167
  ```
114
168
 
169
+ When you navigate from `/products` to `/products/espresso-cup`, only the
170
+ innermost segment changes. Both layouts stay mounted, so they keep their
171
+ client state and aren't re-rendered on the server. A layout can also be a
172
+ server function `(ctx) => <Shell />` when it needs request data, and it can
173
+ carry its own loaders, middleware, and error boundaries. The
174
+ [`/layout` skill](./skills/layout/SKILL.md) covers these, and
175
+ [`/parallel`](./skills/parallel/SKILL.md) covers named slots such as a
176
+ sidebar and main column that load independently.
177
+
178
+ Outside all layouts is the document, the HTML shell passed to
179
+ `createRouter({ document })`:
180
+
115
181
  ```tsx
116
182
  // src/document.tsx
117
183
  "use client";
118
184
 
119
185
  import type { ReactNode } from "react";
120
- import { MetaTags, Scripts } from "@rangojs/router/client";
186
+ import { Html } from "@rangojs/router/client";
121
187
 
122
188
  export function Document({ children }: { children: ReactNode }) {
123
189
  return (
124
190
  <html lang="en">
125
191
  <head>
126
- <MetaTags />
127
- <Scripts />
192
+ <Html.Meta />
193
+ <Html.Scripts />
128
194
  </head>
129
195
  <body>
130
- <Scripts position="body" />
196
+ <Html.Scripts position="body" />
131
197
  {children}
198
+ <Html.ScrollRestoration />
132
199
  </body>
133
200
  </html>
134
201
  );
@@ -157,16 +224,18 @@ const url = ctx.reverse("product", { slug: "espresso-cup" });
157
224
  // "/products/espresso-cup" — name and params compile-time checked
158
225
  ```
159
226
 
160
- Rename `/products/:slug` to `/shop/:slug` in the one place it's defined and
161
- every link, redirect, and prefetch follows. In client components, `href()`
162
- validates static paths against the registered patterns:
163
- `<Link to={href("/")}>Home</Link>`.
227
+ Rename `/products/:slug` to `/shop/:slug` in the one place it's defined, and
228
+ every URL built with `ctx.reverse()` or `useReverse()` follows. In client
229
+ components, `href()` checks a literal path against the registered patterns
230
+ (`<Link to={href("/")}>Home</Link>`). After a rename, a stale `href()` path is
231
+ a compile error, not a broken link.
164
232
 
165
233
  The tree is also lazy-first, which is the shape serverless cold starts want.
166
234
  `include()` mounts a whole route module under a prefix — and with the async
167
235
  form, `include("/shop", () => import("./shop"))`, the group is code-split:
168
236
  its module doesn't load or run until a request matches it, a group nobody
169
- visits never evaluates at all, and warm requests run zero route handlers.
237
+ visits never evaluates at all, and warm requests re-evaluate zero route
238
+ definitions.
170
239
  Boot cost stays flat as the app grows — one module body at startup, not one
171
240
  per group — while matching stays an `O(path length)` prefix trie, identical
172
241
  in dev and production. None of this is assumed: the trie is benchmarked
@@ -230,16 +299,26 @@ export function StockBadge() {
230
299
  }
231
300
  ```
232
301
 
233
- Loaders run in parallel with the handler and stream; `loading()` opts the
234
- segment into skeleton-then-stream. Without it, document requests arrive
235
- **ready** — the HTML ships with data in place; the skeleton is a per-segment
236
- choice, not the first impression.
302
+ Loaders run in parallel with the handler and stream their results.
303
+ `loading()` makes the segment render a skeleton first and stream the data
304
+ into it. Without `loading()`, the segment has no skeleton, and the HTML
305
+ normally arrives with the data already in place. To guarantee that a
306
+ loader's data, head tags, or a real 404 are in the first HTML flush, register
307
+ it with `loader(StockLoader, { ssr: false })`.
237
308
 
238
- The rule of thumb: fetch in the **handler** when the data belongs to the
239
- rendered page — it will be frozen with the shell if you cache it (step 4).
240
- Put data in a **loader** when it must outlive the shell: shared with client
241
- components, fresh on every hit even when the segment is cached, refetchable
242
- from the client, or revalidated on its own after actions.
309
+ When a loader's data changes, every component that reads it updates too.
310
+ Calling `load()` from `useLoader()` refetches it on the client, and
311
+ `createLoader(fn, true)` makes a loader fetchable directly from client code
312
+ with `useFetchLoader()`. See the [`/loader` skill](./skills/loader/SKILL.md).
313
+
314
+ The rule of thumb:
315
+
316
+ - Fetch in the **handler** when the data belongs to the rendered page. If
317
+ you cache the page (step 4), that data is cached with it.
318
+ - Put data in a **loader** when it needs to stay independent of the page:
319
+ shared with client components, fresh on every request even when the page is
320
+ cached, refetchable from the client, or revalidated separately after
321
+ actions.
243
322
 
244
323
  ## 3. Mutations
245
324
 
@@ -251,8 +330,12 @@ JavaScript:
251
330
  // src/actions/cart.ts
252
331
  "use server";
253
332
 
254
- export async function addToCart(productId: string) {
333
+ export type CartState = { added?: string } | null;
334
+
335
+ export async function addToCart(_prev: CartState, formData: FormData) {
336
+ const productId = String(formData.get("productId"));
255
337
  await db.cart.insert({ productId });
338
+ return { added: productId };
256
339
  }
257
340
  ```
258
341
 
@@ -263,26 +346,36 @@ import { useActionState } from "react";
263
346
  import { addToCart } from "../actions/cart";
264
347
 
265
348
  export function AddToCart({ productId }: { productId: string }) {
266
- const [, action, pending] = useActionState(() => addToCart(productId), null);
349
+ const [state, action, pending] = useActionState(addToCart, null);
267
350
  return (
268
351
  <form action={action}>
352
+ <input type="hidden" name="productId" value={productId} />
269
353
  <button disabled={pending}>{pending ? "Adding…" : "Add to cart"}</button>
354
+ {state?.added && <p>Added.</p>}
270
355
  </form>
271
356
  );
272
357
  }
273
358
  ```
274
359
 
275
- After an action, route segments and loaders re-render by default so the UI
276
- reflects the new state. `revalidate()` narrows that to the segments that
277
- actually own the data — matched by action **reference**, so renames are
278
- compile errors, not stale predicates:
360
+ Pass `addToCart` to the hook directly. Wrapping it in a client-side
361
+ closure such as `() => addToCart(id)` turns it into a function that only
362
+ exists in the browser. The form then has no server URL to post to before
363
+ JavaScript loads.
364
+
365
+ After an action, the route's segments and all loaders re-run by default, so
366
+ the UI reflects the new state. Parent layouts are skipped. `revalidate()`
367
+ lets you change that for a segment or loader. It matches actions by
368
+ **reference**, so renaming an action is a compile error, not a check that
369
+ silently stops matching:
279
370
 
280
371
  ```tsx
281
372
  import * as CartActions from "./actions/cart";
282
373
 
283
- path("/cart", CartPage, { name: "cart" }, () => [
284
- loader(CartLoader, () => [
285
- revalidate((ctx) => ctx.isAction(CartActions) || undefined),
374
+ path("/products/:slug", ProductPage, { name: "product" }, () => [
375
+ // After an action, re-run the cart summary only for cart actions.
376
+ // On plain navigation, `undefined` keeps the default behavior.
377
+ loader(CartSummaryLoader, () => [
378
+ revalidate((ctx) => (ctx.isAction() ? ctx.isAction(CartActions) : undefined)),
286
379
  ]),
287
380
  ]),
288
381
  ```
@@ -302,21 +395,31 @@ choice from step 2 pays off: handler data freezes with the shell, the
302
395
 
303
396
  ```tsx
304
397
  const urlpatterns = urls(({ path, layout, loader, loading, cache }) => [
305
- layout(<ShopLayout />, () => [
398
+ layout(<SiteLayout />, () => [
306
399
  path("/", HomePage, { name: "home" }),
307
- cache({ ttl: 600, swr: 3600, tags: ["products"] }, () => [
308
- path("/products/:slug", ProductPage, { name: "product" }, () => [
309
- loader(StockLoader), // never cached: re-runs on every hit
310
- loading(<ProductSkeleton />),
400
+
401
+ layout(<ShopLayout />, () => [
402
+ path("/products", ProductList, { name: "products" }),
403
+ cache({ ttl: 600, swr: 3600, tags: ["products"] }, () => [
404
+ path("/products/:slug", ProductPage, { name: "product" }, () => [
405
+ loader(StockLoader), // never cached: re-runs on every hit
406
+ loading(<ProductSkeleton />),
407
+ ]),
311
408
  ]),
312
409
  ]),
313
410
  ]),
314
411
  ]);
315
412
  ```
316
413
 
317
- Wire a store once on the router (`MemorySegmentCacheStore` for dev,
318
- `CFCacheStore` for Cloudflare — see the [`/caching` skill](./skills/caching/SKILL.md)),
319
- and bust by tag from the mutation that changes the data:
414
+ Set up a store once on the router. Use `MemorySegmentCacheStore` in
415
+ development, `CFCacheStore` on Cloudflare (Cache API with optional KV), or
416
+ `VercelCacheStore` on Vercel. The memory store expires entries at `ttl` and
417
+ does not serve stale content in the background, so you will only see `swr`
418
+ work on the Cloudflare and Vercel stores. The
419
+ [`/caching` skill](./skills/caching/SKILL.md) covers setup.
420
+
421
+ To invalidate, call `updateTag()` with the tag from the action that changes
422
+ the data:
320
423
 
321
424
  ```tsx
322
425
  // src/actions/products.ts
@@ -329,27 +432,40 @@ export async function renameProduct(id: string, name: string) {
329
432
  }
330
433
  ```
331
434
 
332
- Navigation speed is a `Link` prop away:
435
+ In production, links prefetch when they scroll into view. You can change
436
+ that per link with `prefetch`: `"hover"`, `"viewport"`, `"render"`,
437
+ `"adaptive"` (hover on pointer devices, viewport on touch), or `"none"`:
333
438
 
334
439
  ```tsx
335
- <Link to={url} prefetch="viewport">
440
+ <Link to={url} prefetch="adaptive">
336
441
  {product.name}
337
442
  </Link>
338
443
  ```
339
444
 
340
- A fully-prefetched navigation commits a **finished page** — no skeleton, no
341
- loading flash — and staying correct is the router's job: every action
342
- invalidates the prefetch caches by default, so a prefetched page can't show
343
- pre-mutation data.
344
-
345
- To move the shell's cost to build time entirely, `Prerender()` bakes it while
346
- loaders stay live at runtime — same mental model, earlier cache write. See
347
- the [`/prerender` skill](./skills/prerender/SKILL.md).
445
+ When a navigation has been fully prefetched, the page appears finished, with
446
+ no skeleton or loading flash. If you click while a prefetch is still loading,
447
+ the navigation reuses that request instead of starting a new one. Every
448
+ action clears the prefetch caches by default, so a prefetched page can't
449
+ show data from before a mutation.
450
+
451
+ You can do the same caching work at other times:
452
+
453
+ - **At build time:** `Prerender()` renders the page during `vite build` and
454
+ stores it in the same format as a runtime cache entry. Loaders still run
455
+ on every request. The browser can't tell a prerendered page from a cached
456
+ one. See the [`/prerender` skill](./skills/prerender/SKILL.md).
457
+ - **As an HTML shell:** `{ name: "product", ppr: true }` on a path stores
458
+ the static HTML around the loaders. On a hit, the router sends that shell
459
+ once middleware has run, then streams in only the live parts. See the
460
+ [`/ppr` skill](./skills/ppr/SKILL.md).
461
+ - **Per function:** `"use cache"` caches a single function or component,
462
+ with named profiles. See the [`/use-cache` skill](./skills/use-cache/SKILL.md).
348
463
 
349
464
  ## 5. An API, when you need one
350
465
 
351
- Response routes live in the same tree — `path.json()`, `path.text()`,
352
- `path.xml()`, `path.image()`, `path.stream()`:
466
+ Response routes live in the same tree: `path.json()`, `path.text()`,
467
+ `path.html()`, `path.xml()`, `path.md()`, `path.image()`, `path.stream()`,
468
+ and `path.any()` for raw `Response`s such as WebSocket upgrades:
353
469
 
354
470
  ```tsx
355
471
  path("/products/:slug", ProductPage, { name: "product" }),
@@ -370,11 +486,52 @@ type Product = RouteResponse<typeof urlpatterns, "productJson">;
370
486
  See the [`/api-client` skill](./skills/api-client/SKILL.md) for a small typed
371
487
  client over these endpoints.
372
488
 
489
+ ## 6. Tests
490
+
491
+ Everything above can be tested at its own layer, through the same public
492
+ APIs your app uses. A loader runs in plain Node with the real request
493
+ context, and you supply the params:
494
+
495
+ ```ts
496
+ // src/loaders/stock.test.ts
497
+ import { runLoader } from "@rangojs/router/testing";
498
+ import { StockLoader } from "./stock";
499
+
500
+ test("reports stock for a product", async () => {
501
+ const stock = await runLoader(StockLoader, {
502
+ params: { slug: "espresso-cup" },
503
+ });
504
+ expect(stock.inStock).toBe(true);
505
+ });
506
+ ```
507
+
508
+ End to end, `parityDescribe` runs one test body against the dev server and
509
+ the production build. `expectParity` submits the form once with JavaScript
510
+ and once without, then checks that both show the same result:
511
+
512
+ ```ts
513
+ // e2e/cart.test.ts
514
+ parityDescribe("add to cart", (f) => {
515
+ test("works the same with and without JavaScript", async ({ page }) => {
516
+ await page.goto(f.url("/products/espresso-cup"));
517
+ await expectParity(
518
+ page,
519
+ { submit: { testId: "add-to-cart-form" } },
520
+ { observe: ["cart-count"] },
521
+ );
522
+ });
523
+ });
524
+ ```
525
+
526
+ `runMiddleware`, `renderHandler` (Server Component handlers rendered to a
527
+ real Flight tree), `renderRoute` (DOM tests) and the cache assertions follow
528
+ the same pattern. See the [`/testing` skill](./skills/testing/SKILL.md).
529
+
373
530
  ## Everything else, when you need it
374
531
 
375
- That was the core: `path`/`layout`/`include`, names, loaders, actions +
376
- `revalidate`, `cache`, response routes. The rest is opt-in — reach for it
377
- when the requirement appears:
532
+ That was the core: `path`/`layout`/`include`, names, loaders, actions and
533
+ `revalidate`, `cache`, response routes, and tests. Everything else is opt-in.
534
+ Use it when you need it:
378
535
 
379
536
  | I need to… | Skill |
380
537
  | ----------------------------------------------- | -------------------------------------------------------------------------------------------- |
@@ -404,25 +561,28 @@ mental model that ties it together.
404
561
 
405
562
  ### Imports and subpaths
406
563
 
407
- | Export | Description |
408
- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
409
- | `@rangojs/router` | Server/RSC core and shared types: `createRouter`, `urls`, `createLoader`, `Handler`, `Prerender`, `Meta` |
410
- | `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `MetaTags` |
411
- | `@rangojs/router/cache` | Cache: `CFCacheStore`, `VercelCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware` |
412
- | `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript` |
413
- | `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts`, `isNoRouteMatchError` |
414
- | `@rangojs/router/vercel` | Vercel: `createVercelTracing` (phase spans via `@vercel/otel`'s global tracer) |
415
- | `@rangojs/router/vite` | Vite plugin: `rango()` |
416
- | `@rangojs/router/testing` | Consumer testing primitives: `runLoader`, `runMiddleware`, `dispatch` (plus `/testing/dom`, `/testing/flight`, `/testing/e2e`) |
417
- | `@rangojs/router/rsc` | Advanced server pipeline APIs: `createRSCHandler`, request-context access |
418
- | `@rangojs/router/ssr` | Advanced SSR bridge APIs: `createSSRHandler` |
564
+ | Export | Description |
565
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
566
+ | `@rangojs/router` | Server/RSC core and shared types: `createRouter`, `urls`, `createLoader`, `Handler`, `Prerender`, `Meta` |
567
+ | `@rangojs/router/client` | Client: `Link`, `Outlet`, `href`, `useNavigation`, `useLoader`, `Html` |
568
+ | `@rangojs/router/cache` | Cache: `CFCacheStore`, `VercelCacheStore`, `MemorySegmentCacheStore`, `createDocumentCacheMiddleware` |
569
+ | `@rangojs/router/theme` | Theme: `useTheme`, `ThemeProvider`, `ThemeScript` |
570
+ | `@rangojs/router/host` | Host routing: `createHostRouter`, `defineHosts`, `isNoRouteMatchError` |
571
+ | `@rangojs/router/vercel` | Vercel: `createVercelTracing` (phase spans via `@vercel/otel`'s global tracer) |
572
+ | `@rangojs/router/cloudflare` | Cloudflare: `createCloudflareTracing` |
573
+ | `@rangojs/router/vite` | Vite plugin: `rango()` |
574
+ | `@rangojs/router/testing` | Consumer testing primitives: `runLoader`, `runMiddleware`, `dispatch`, cache assertions |
575
+ | `@rangojs/router/testing/*` | `/dom` (`renderRoute`), `/flight` (`renderHandler`), `/flight-matchers`, `/e2e` (`createRangoE2E`), `/vitest` (config helpers) |
576
+ | `@rangojs/router/host/testing` | Test helpers for host routers |
577
+ | `@rangojs/router/rsc` | Advanced server pipeline APIs: `createRSCHandler`, request-context access |
578
+ | `@rangojs/router/ssr` | Advanced SSR bridge APIs: `createSSRHandler` |
419
579
 
420
580
  Use only subpaths that are explicitly exported; avoid deep imports.
421
581
 
422
582
  The root entry is conditionally resolved: server-only APIs (`createRouter`,
423
583
  `urls`, `redirect`, `Prerender`, `cookies`) run under the `react-server`
424
584
  condition and throw guidance errors elsewhere. If you hit a root-entrypoint
425
- stub error: hooks and components (`Link`, `Outlet`, `useLoader`, `MetaTags`)
585
+ stub error: hooks and components (`Link`, `Outlet`, `useLoader`, `Html`)
426
586
  live in `@rangojs/router/client`; cache APIs in `@rangojs/router/cache`;
427
587
  host APIs in `@rangojs/router/host`.
428
588
 
@@ -431,14 +591,19 @@ host APIs in `@rangojs/router/host`.
431
591
  The Vite plugin generates `router.named-routes.gen.ts` automatically (on dev
432
592
  startup, HMR, and builds), registering route names, params, and search
433
593
  schemas globally via `Rango.GeneratedRouteMap`. That powers `Handler<"name">`,
434
- `ctx.reverse()`, and `RouteParams<"name">` with no manual registration.
594
+ `ctx.reverse()`, `href()`, `Rango.Path` and `RouteParams<"name">` with no
595
+ manual registration. `RouteResponse<typeof urlpatterns, "name">` takes the
596
+ patterns directly, so it needs no registration either.
435
597
 
436
- For response-aware and path-based utilities (`href()`, `Rango.Path`,
437
- `RouteResponse`), augment `Rango.RegisteredRoutes` once:
598
+ Two things are opt-in augmentations. `Rango.Env` types `ctx.env`.
599
+ `Rango.RegisteredRoutes` is needed only for `Rango.PathResponse` (looking up
600
+ a response payload by path):
438
601
 
439
602
  ```ts
440
603
  // router.tsx
441
- const router = createRouter<AppBindings>({}).routes(urlpatterns);
604
+ type AppBindings = { DB: D1Database };
605
+
606
+ export const router = createRouter<AppBindings>({}).routes(urlpatterns);
442
607
 
443
608
  // The alias is required: an interface heritage clause cannot take a `typeof`
444
609
  // type query directly (TS1109), so extend through a named alias.
@@ -446,7 +611,7 @@ type AppRoutes = typeof router.routeMap;
446
611
 
447
612
  declare global {
448
613
  namespace Rango {
449
- interface Env extends AppEnv {}
614
+ interface Env extends AppBindings {}
450
615
  interface RegisteredRoutes extends AppRoutes {}
451
616
  }
452
617
  }