@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1

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 (214) hide show
  1. package/README.md +9 -9
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +914 -485
  5. package/package.json +55 -11
  6. package/skills/bundle-analysis/SKILL.md +159 -0
  7. package/skills/cache-guide/SKILL.md +220 -30
  8. package/skills/caching/SKILL.md +116 -8
  9. package/skills/composability/SKILL.md +27 -2
  10. package/skills/document-cache/SKILL.md +78 -55
  11. package/skills/handler-use/SKILL.md +3 -1
  12. package/skills/hooks/SKILL.md +214 -18
  13. package/skills/host-router/SKILL.md +45 -20
  14. package/skills/intercept/SKILL.md +26 -4
  15. package/skills/layout/SKILL.md +6 -7
  16. package/skills/links/SKILL.md +173 -17
  17. package/skills/loader/SKILL.md +149 -6
  18. package/skills/middleware/SKILL.md +13 -9
  19. package/skills/migrate-nextjs/SKILL.md +1 -1
  20. package/skills/mime-routes/SKILL.md +27 -0
  21. package/skills/observability/SKILL.md +137 -0
  22. package/skills/parallel/SKILL.md +5 -6
  23. package/skills/prerender/SKILL.md +14 -33
  24. package/skills/rango/SKILL.md +242 -26
  25. package/skills/react-compiler/SKILL.md +168 -0
  26. package/skills/response-routes/SKILL.md +58 -9
  27. package/skills/route/SKILL.md +13 -4
  28. package/skills/router-setup/SKILL.md +3 -3
  29. package/skills/server-actions/SKILL.md +53 -41
  30. package/skills/testing/SKILL.md +599 -0
  31. package/skills/typesafety/SKILL.md +310 -26
  32. package/skills/use-cache/SKILL.md +34 -5
  33. package/skills/view-transitions/SKILL.md +294 -0
  34. package/src/__augment-tests__/augment.ts +81 -0
  35. package/src/__augment-tests__/augmented.check.ts +117 -0
  36. package/src/browser/action-coordinator.ts +53 -36
  37. package/src/browser/event-controller.ts +42 -66
  38. package/src/browser/history-state.ts +21 -0
  39. package/src/browser/index.ts +3 -3
  40. package/src/browser/navigation-bridge.ts +6 -6
  41. package/src/browser/navigation-client.ts +12 -15
  42. package/src/browser/navigation-store.ts +7 -8
  43. package/src/browser/navigation-transaction.ts +10 -28
  44. package/src/browser/partial-update.ts +9 -19
  45. package/src/browser/react/NavigationProvider.tsx +29 -40
  46. package/src/browser/react/index.ts +3 -0
  47. package/src/browser/react/location-state-shared.ts +175 -4
  48. package/src/browser/react/location-state.ts +39 -13
  49. package/src/browser/react/use-handle.ts +17 -9
  50. package/src/browser/react/use-params.ts +3 -4
  51. package/src/browser/react/use-reverse.ts +106 -0
  52. package/src/browser/react/use-router.ts +14 -1
  53. package/src/browser/response-adapter.ts +25 -0
  54. package/src/browser/rsc-router.tsx +30 -16
  55. package/src/browser/scroll-restoration.ts +22 -14
  56. package/src/browser/segment-structure-assert.ts +2 -2
  57. package/src/browser/server-action-bridge.ts +23 -30
  58. package/src/browser/types.ts +2 -0
  59. package/src/build/collect-fallback-refs.ts +107 -0
  60. package/src/build/generate-manifest.ts +60 -35
  61. package/src/build/generate-route-types.ts +2 -0
  62. package/src/build/index.ts +2 -0
  63. package/src/build/route-types/codegen.ts +4 -4
  64. package/src/build/route-types/include-resolution.ts +1 -1
  65. package/src/build/route-types/per-module-writer.ts +7 -4
  66. package/src/build/route-types/router-processing.ts +55 -14
  67. package/src/build/route-types/scan-filter.ts +1 -1
  68. package/src/build/route-types/source-scan.ts +118 -0
  69. package/src/build/runtime-discovery.ts +9 -20
  70. package/src/cache/cache-scope.ts +28 -42
  71. package/src/cache/cf/cf-cache-store.ts +49 -6
  72. package/src/client.rsc.tsx +3 -0
  73. package/src/client.tsx +10 -8
  74. package/src/context-var.ts +5 -5
  75. package/src/decode-loader-results.ts +36 -0
  76. package/src/errors.ts +30 -1
  77. package/src/handle.ts +26 -13
  78. package/src/host/index.ts +2 -2
  79. package/src/host/router.ts +129 -57
  80. package/src/host/types.ts +31 -2
  81. package/src/host/utils.ts +1 -1
  82. package/src/href-client.ts +140 -20
  83. package/src/index.rsc.ts +6 -4
  84. package/src/index.ts +13 -6
  85. package/src/loader-store.ts +500 -0
  86. package/src/loader.rsc.ts +2 -5
  87. package/src/loader.ts +3 -10
  88. package/src/missing-id-error.ts +68 -0
  89. package/src/prerender.ts +4 -4
  90. package/src/response-utils.ts +9 -0
  91. package/src/reverse.ts +65 -41
  92. package/src/route-content-wrapper.tsx +6 -28
  93. package/src/route-definition/dsl-helpers.ts +238 -263
  94. package/src/route-definition/helper-factories.ts +29 -139
  95. package/src/route-definition/helpers-types.ts +37 -14
  96. package/src/route-definition/use-item-types.ts +32 -0
  97. package/src/route-types.ts +19 -41
  98. package/src/router/basename.ts +14 -0
  99. package/src/router/content-negotiation.ts +15 -2
  100. package/src/router/error-handling.ts +1 -1
  101. package/src/router/handler-context.ts +4 -42
  102. package/src/router/intercept-resolution.ts +4 -18
  103. package/src/router/lazy-includes.ts +2 -2
  104. package/src/router/loader-resolution.ts +16 -2
  105. package/src/router/match-handlers.ts +62 -20
  106. package/src/router/match-middleware/cache-lookup.ts +44 -91
  107. package/src/router/match-middleware/cache-store.ts +3 -2
  108. package/src/router/match-result.ts +32 -30
  109. package/src/router/metrics.ts +1 -1
  110. package/src/router/middleware-types.ts +1 -1
  111. package/src/router/middleware.ts +46 -78
  112. package/src/router/prerender-match.ts +1 -1
  113. package/src/router/preview-match.ts +3 -1
  114. package/src/router/request-classification.ts +4 -28
  115. package/src/router/revalidation.ts +43 -1
  116. package/src/router/router-interfaces.ts +45 -28
  117. package/src/router/router-options.ts +40 -1
  118. package/src/router/router-registry.ts +2 -5
  119. package/src/router/segment-resolution/fresh.ts +19 -6
  120. package/src/router/segment-resolution/revalidation.ts +19 -6
  121. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  122. package/src/router/substitute-pattern-params.ts +56 -0
  123. package/src/router/telemetry.ts +99 -0
  124. package/src/router/types.ts +8 -0
  125. package/src/router.ts +37 -21
  126. package/src/rsc/handler-context.ts +2 -2
  127. package/src/rsc/handler.ts +20 -65
  128. package/src/rsc/helpers.ts +22 -2
  129. package/src/rsc/index.ts +1 -1
  130. package/src/rsc/origin-guard.ts +28 -10
  131. package/src/rsc/response-route-handler.ts +32 -52
  132. package/src/rsc/rsc-rendering.ts +27 -53
  133. package/src/rsc/runtime-warnings.ts +9 -10
  134. package/src/rsc/server-action.ts +13 -37
  135. package/src/rsc/ssr-setup.ts +16 -0
  136. package/src/rsc/types.ts +2 -2
  137. package/src/search-params.ts +4 -4
  138. package/src/segment-system.tsx +121 -65
  139. package/src/serialize.ts +243 -0
  140. package/src/server/context.ts +118 -51
  141. package/src/server/cookie-store.ts +28 -4
  142. package/src/server/request-context.ts +10 -0
  143. package/src/static-handler.ts +1 -1
  144. package/src/testing/cache-status.ts +166 -0
  145. package/src/testing/collect-handle.ts +63 -0
  146. package/src/testing/dispatch.ts +440 -0
  147. package/src/testing/dom.entry.ts +22 -0
  148. package/src/testing/e2e/fixture.ts +154 -0
  149. package/src/testing/e2e/index.ts +149 -0
  150. package/src/testing/e2e/matchers.ts +51 -0
  151. package/src/testing/e2e/page-helpers.ts +272 -0
  152. package/src/testing/e2e/parity.ts +306 -0
  153. package/src/testing/e2e/server.ts +183 -0
  154. package/src/testing/flight-matchers.ts +104 -0
  155. package/src/testing/flight-runtime.d.ts +21 -0
  156. package/src/testing/flight.entry.ts +22 -0
  157. package/src/testing/flight.ts +182 -0
  158. package/src/testing/generated-routes.ts +223 -0
  159. package/src/testing/index.ts +105 -0
  160. package/src/testing/internal/context.ts +193 -0
  161. package/src/testing/render-route.tsx +536 -0
  162. package/src/testing/run-loader.ts +296 -0
  163. package/src/testing/run-middleware.ts +170 -0
  164. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  165. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  166. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  167. package/src/testing/vitest-stubs/version.ts +5 -0
  168. package/src/testing/vitest.ts +183 -0
  169. package/src/types/global-namespace.ts +39 -26
  170. package/src/types/handler-context.ts +56 -11
  171. package/src/types/index.ts +1 -0
  172. package/src/types/segments.ts +18 -1
  173. package/src/urls/include-helper.ts +10 -53
  174. package/src/urls/index.ts +0 -3
  175. package/src/urls/path-helper-types.ts +11 -3
  176. package/src/urls/path-helper.ts +17 -52
  177. package/src/urls/pattern-types.ts +36 -19
  178. package/src/urls/response-types.ts +20 -19
  179. package/src/urls/type-extraction.ts +26 -116
  180. package/src/urls/urls-function.ts +1 -5
  181. package/src/use-loader.tsx +413 -42
  182. package/src/vite/debug.ts +1 -0
  183. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  184. package/src/vite/discovery/discover-routers.ts +70 -48
  185. package/src/vite/discovery/discovery-errors.ts +194 -0
  186. package/src/vite/discovery/prerender-collection.ts +19 -25
  187. package/src/vite/discovery/route-types-writer.ts +40 -84
  188. package/src/vite/discovery/state.ts +33 -0
  189. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  190. package/src/vite/index.ts +2 -0
  191. package/src/vite/plugin-types.ts +67 -0
  192. package/src/vite/plugins/cjs-to-esm.ts +3 -7
  193. package/src/vite/plugins/client-ref-hashing.ts +12 -1
  194. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
  195. package/src/vite/plugins/expose-action-id.ts +2 -2
  196. package/src/vite/plugins/expose-id-utils.ts +12 -8
  197. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  198. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  199. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  200. package/src/vite/plugins/expose-internal-ids.ts +47 -67
  201. package/src/vite/plugins/performance-tracks.ts +12 -16
  202. package/src/vite/plugins/use-cache-transform.ts +13 -11
  203. package/src/vite/plugins/version-injector.ts +2 -12
  204. package/src/vite/plugins/version-plugin.ts +59 -2
  205. package/src/vite/plugins/virtual-entries.ts +2 -2
  206. package/src/vite/rango.ts +67 -15
  207. package/src/vite/router-discovery.ts +208 -63
  208. package/src/vite/utils/ast-handler-extract.ts +15 -15
  209. package/src/vite/utils/bundle-analysis.ts +4 -2
  210. package/src/vite/utils/client-chunks.ts +190 -0
  211. package/src/vite/utils/forward-user-plugins.ts +193 -0
  212. package/src/vite/utils/manifest-utils.ts +21 -5
  213. package/src/vite/utils/shared-utils.ts +107 -26
  214. package/src/browser/action-response-classifier.ts +0 -99
@@ -0,0 +1,168 @@
1
+ ---
2
+ name: react-compiler
3
+ description: Enable the React Compiler in a Rango app the @vitejs/plugin-rsc way — a separate @rolldown/plugin-babel running reactCompilerPreset(), ordered after react() and before the plugin that supplies @vitejs/plugin-rsc. Use when a consumer wants to turn React Compiler on, hits the dead plugin-react v6 `react({ babel })` path, or is unsure why server components aren't being compiled.
4
+ argument-hint:
5
+ ---
6
+
7
+ # React Compiler
8
+
9
+ React Compiler is **opt-in** in Rango. The plugin pipeline is fully compatible —
10
+ you just add one more plugin. The catch on a current Rango stack (Vite 8 +
11
+ `@vitejs/plugin-react` v6) is that **v6 dropped its internal Babel for oxc**, so
12
+ the way the React docs and most blog posts show it — `react({ babel: { plugins:
13
+ [...] } })` — silently does nothing. The compiler has to be its own top-level
14
+ plugin.
15
+
16
+ ## The shape (read first)
17
+
18
+ - The compiler is a **Babel** plugin, run via
19
+ [`@rolldown/plugin-babel`](https://www.npmjs.com/package/@rolldown/plugin-babel)
20
+ with `reactCompilerPreset()` from `@vitejs/plugin-react`.
21
+ - **Ordering is load-bearing:** put `babel(...)` **after `react()`** and
22
+ **before the plugin that supplies `@vitejs/plugin-rsc`**. In a default Rango
23
+ app that plugin is `rango()` itself; in a Cloudflare app it is
24
+ `@cloudflare/vite-plugin`.
25
+ - **It is client-only.** `reactCompilerPreset()` gates itself to the client
26
+ environment. Server/RSC components are not compiled, and that is the upstream
27
+ example's behavior — not a Rango limitation. See
28
+ [What gets compiled](#what-gets-compiled-client-only).
29
+ - **Rango's build-time prerender is unaffected.** You do not need to do anything
30
+ special. See [Prerender](#interaction-with-build-time-prerender).
31
+
32
+ ## Step 1: Install
33
+
34
+ ```bash
35
+ pnpm add -D @rolldown/plugin-babel @babel/core babel-plugin-react-compiler
36
+ # TypeScript users also want the Babel core types:
37
+ pnpm add -D @types/babel__core
38
+ ```
39
+
40
+ React 19 ships `react/compiler-runtime` in-tree, so there is **no** extra runtime
41
+ to install and **no** `target` option to set. Only pass `target: '17' | '18'` to
42
+ `reactCompilerPreset()` if you are on an older React.
43
+
44
+ ## Step 2: Wire it in
45
+
46
+ ### Default (non-Cloudflare) app
47
+
48
+ ```ts
49
+ // vite.config.ts
50
+ import { defineConfig } from "vite";
51
+ import react, { reactCompilerPreset } from "@vitejs/plugin-react";
52
+ import babel from "@rolldown/plugin-babel";
53
+ import { rango } from "@rangojs/router/vite";
54
+
55
+ export default defineConfig({
56
+ plugins: [
57
+ react(),
58
+ babel({ presets: [reactCompilerPreset()] }),
59
+ rango(), // supplies @vitejs/plugin-rsc
60
+ ],
61
+ });
62
+ ```
63
+
64
+ ### Cloudflare app
65
+
66
+ ```ts
67
+ // vite.config.ts
68
+ import { cloudflare } from "@cloudflare/vite-plugin";
69
+ import react, { reactCompilerPreset } from "@vitejs/plugin-react";
70
+ import babel from "@rolldown/plugin-babel";
71
+ import { defineConfig } from "vite";
72
+ import { rango } from "@rangojs/router/vite";
73
+
74
+ export default defineConfig({
75
+ plugins: [
76
+ react(),
77
+ babel({ presets: [reactCompilerPreset()] }),
78
+ rango({ preset: "cloudflare" }),
79
+ cloudflare({
80
+ /* ... */
81
+ }), // supplies @vitejs/plugin-rsc
82
+ ],
83
+ });
84
+ ```
85
+
86
+ ## What gets compiled (client-only)
87
+
88
+ `reactCompilerPreset()` carries
89
+ `rolldown.applyToEnvironmentHook: (env) => env.config.consumer === "client"`, so
90
+ even though the babel plugin is top-level, the transform runs **only in the
91
+ `client` environment**:
92
+
93
+ | Environment | `consumer` | Compiled? |
94
+ | ----------- | ---------- | --------- |
95
+ | client | `client` | Yes |
96
+ | ssr | `server` | No |
97
+ | rsc | `server` | No |
98
+
99
+ This matches the upstream `@vitejs/plugin-rsc` example. If you genuinely need to
100
+ compile **server** components, you would have to invoke
101
+ `babel-plugin-react-compiler` yourself without the preset's
102
+ `applyToEnvironmentHook` — that is outside what the example does and is not
103
+ covered here.
104
+
105
+ ## Options
106
+
107
+ `reactCompilerPreset()` forwards to `babel-plugin-react-compiler`:
108
+
109
+ | Option | Effect |
110
+ | ------------------------------- | -------------------------------------------------------------------------------------- |
111
+ | `compilationMode: 'annotation'` | Compile only components marked with the `"use memo"` directive, not every eligible one |
112
+ | `target: '17' \| '18'` | Emit `react-compiler-runtime` calls for React < 19. Omit on React 19+. |
113
+
114
+ ## Interaction with build-time prerender
115
+
116
+ Nothing to configure. Rango's discovery/prerender step runs a throwaway temp Vite
117
+ server (`createTempRscServer`) that forwards only your **resolution** plugins
118
+ (`resolveId` / `load`). A pure transform plugin like `@rolldown/plugin-babel` is
119
+ intentionally **not** forwarded — and that is correct: the temp runner only
120
+ produces **data** (serialized Flight payloads + the route manifest), not shipped
121
+ code, and React Compiler is a memoization-only transform that does not change
122
+ rendered output. Your shipped client bundle still gets compiled, because the
123
+ babel plugin lives in your app's top-level plugin array alongside `react()`.
124
+
125
+ ## Step 3: Verify the compiler actually ran
126
+
127
+ A compiled module imports the cache allocator from `react/compiler-runtime` and
128
+ calls `_c(n)`. Those two appear in **every** compiled module, so they are the
129
+ reliable per-module signal in dev:
130
+
131
+ ```bash
132
+ pnpm dev
133
+ # fetch any client component module straight from Vite and look for the markers:
134
+ curl -s "http://localhost:5173/src/components/SomeClientComponent.tsx" \
135
+ | grep -E "compiler-runtime|_c\("
136
+ ```
137
+
138
+ For a production build, grep the built client bundle for the compiler's
139
+ input-independent cache check, which has a **zero baseline** without the compiler:
140
+
141
+ ```bash
142
+ pnpm build
143
+ grep -r "Symbol.for(\"react.memo_cache_sentinel\")" dist/client/assets/ | head
144
+ ```
145
+
146
+ Note the **comparison** form `$[i] === Symbol.for("react.memo_cache_sentinel")`
147
+ is only emitted for components with input-independent JSX, so it is reliable over
148
+ the **whole** client bundle, not necessarily in one chosen module. (React core
149
+ also defines that symbol once with a single `=` assignment, so count comparisons,
150
+ not the bare string.) Run the same grep over `dist/rsc` / `dist/ssr` and you
151
+ should find **none** — that is the client-only contract.
152
+
153
+ ## Troubleshooting
154
+
155
+ | Symptom | Cause / fix |
156
+ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
157
+ | Nothing is compiled; no `compiler-runtime` import anywhere | You used `react({ babel: { plugins: [...] } })`. plugin-react v6 has no internal Babel — add `@rolldown/plugin-babel` as its own plugin. |
158
+ | Client compiled, but server/RSC components are not | Expected. `reactCompilerPreset()` is client-only (see the table). Not a bug. |
159
+ | `Cannot find module 'babel-plugin-react-compiler'` (or `@babel/core`) | Install the peer deps from Step 1; they are not bundled by `reactCompilerPreset()`. |
160
+ | Build pulls in `react-compiler-runtime` | You set `target: '17'`/`'18'` on React 19. Drop `target` — React 19 ships `react/compiler-runtime` in-tree. |
161
+ | Output looks compiled but a component misbehaves | The component likely breaks the Rules of React. Fix the component, or scope the compiler with `compilationMode: 'annotation'` while you do. |
162
+
163
+ ## Reference
164
+
165
+ A worked, tested wiring (dev + production e2e markers, incl. the client-only
166
+ contract) lives in the `@rangojs/router` repo: `docs/react-compiler.md` and the
167
+ `react-compiler.test.ts` files under `e2e/e2e-basic`, `tests/cloudflare-basic`,
168
+ and `tests/vite-rsc-demo`.
@@ -236,22 +236,69 @@ type ProductsData = RouteResponse<typeof apiPatterns, "products">;
236
236
  // = ResponseEnvelope<{ id: string; name: string; price: number }[]>
237
237
  ```
238
238
 
239
- ### PathResponse (global lookup by URL pattern)
239
+ ### Rango.PathResponse (global lookup by URL pattern or concrete path)
240
240
 
241
- Look up response type from the merged route map by URL pattern:
241
+ `Rango.PathResponse` is ambient (no import) and reads from `RegisteredRoutes`,
242
+ which carries response payload metadata. That surface is **not** auto-wired —
243
+ without the augmentation below, `Rango.PathResponse` falls back to the generated
244
+ path/search map, or to a permissive map when nothing is generated. Either way, it
245
+ has no response payload metadata, so response routes resolve to
246
+ `ResponseEnvelope<never>`:
242
247
 
243
248
  ```typescript
244
- import type { PathResponse } from "@rangojs/router/client";
249
+ // router.tsx
250
+ export const router = createRouter({ document: Document }).routes(urlpatterns);
245
251
 
252
+ declare global {
253
+ namespace Rango {
254
+ interface RegisteredRoutes extends typeof router.routeMap {}
255
+ }
256
+ }
257
+ ```
258
+
259
+ With that in place, look up the response type by URL pattern (ambient, no import):
260
+
261
+ ```typescript
246
262
  // After include("/api", apiPatterns) in main urls
247
- type Health = PathResponse<"/api/health">;
263
+ type Health = Rango.PathResponse<"/api/health">;
248
264
  // = ResponseEnvelope<{ status: string; timestamp: number }>
249
265
 
250
266
  // RSC routes return ResponseEnvelope<never>
251
- type Home = PathResponse<"/">;
267
+ type Home = Rango.PathResponse<"/">;
252
268
  // = ResponseEnvelope<never>
253
269
  ```
254
270
 
271
+ `Rango.PathResponse` also accepts a **concrete path**, so it types a `fetch`
272
+ wrapper whose response is inferred from the path you pass:
273
+
274
+ ```typescript
275
+ import { href } from "@rangojs/router/client";
276
+
277
+ async function get<T extends Rango.Path>(
278
+ path: T,
279
+ ): Promise<Rango.PathResponse<T>> {
280
+ return fetch(href(path)).then((r) => r.json());
281
+ }
282
+
283
+ const product = await get("/api/products/42"); // ResponseEnvelope<Product>
284
+ ```
285
+
286
+ Pattern keys (`/:id`) match exactly; a concrete path under a _nested_ dynamic
287
+ route can match several patterns and union their responses.
288
+
289
+ `Rango.PathResponse` reports the JSON **wire** shape, not the handler's raw
290
+ return: `path.json()` serializes with `JSON.stringify`, so a handler returning
291
+ `{ createdAt: Date }` resolves to `ResponseEnvelope<{ createdAt: string }>`. This
292
+ runs through the ambient `Rango.JsonSerialize<T>` transform (`Date -> string`,
293
+ honors `toJSON()`, drops functions/`undefined`, `bigint -> never`). The
294
+ `RouteResponse` surface below applies the same `Rango.JsonSerialize` transform, so
295
+ both response lookups report the identical wire shape.
296
+
297
+ For local/scoped response typing without global augmentation, prefer
298
+ `RouteResponse<typeof patterns, "routeName">` (see the section above) — it reads
299
+ the response payload straight from the `urls()` patterns and needs no
300
+ `RegisteredRoutes` wiring.
301
+
255
302
  ### ParamsFor with Response Routes
256
303
 
257
304
  ```typescript
@@ -361,14 +408,16 @@ export const urlpatterns = urls(({ path, include }) => [
361
408
 
362
409
  ```typescript
363
410
  import type { RouteResponse } from "@rangojs/router";
364
- import type { PathResponse, ParamsFor } from "@rangojs/router/client";
411
+ import type { ParamsFor } from "@rangojs/router/client";
365
412
 
366
- // Scoped (before mount) -- use the module directly
413
+ // Scoped (before mount) -- use the module directly, no global wiring needed
367
414
  type Stats = RouteResponse<typeof blogApiPatterns, "stats">;
368
415
  // = ResponseEnvelope<{ views: number; visitors: number }>
369
416
 
370
- // After mounting -- names get prefixed
371
- type BlogStats = PathResponse<"/blog/api/stats">;
417
+ // After mounting -- names get prefixed.
418
+ // Rango.PathResponse needs `RegisteredRoutes extends typeof router.routeMap` (see above),
419
+ // otherwise it resolves to ResponseEnvelope<never>.
420
+ type BlogStats = Rango.PathResponse<"/blog/api/stats">;
372
421
  // = ResponseEnvelope<{ views: number; visitors: number }>
373
422
 
374
423
  // Params work through nested includes
@@ -234,14 +234,22 @@ Cacheable vars (the default) can be read freely inside cache scopes.
234
234
 
235
235
  ### Revalidation Contracts for Handler Data
236
236
 
237
+ > **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
238
+ > It decides whether this segment re-runs and streams to the client on a
239
+ > navigation or action — never whether a cached value is stale. The cache
240
+ > decides hit/miss/ttl/swr independently and never reads `revalidate()`. See
241
+ > `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
242
+
237
243
  Handler-first guarantees apply within a single full render pass. For partial
238
244
  action revalidation, define named revalidation contracts and reuse them on both
239
245
  the producer route and the consumer child segments.
240
246
 
241
247
  ```typescript
242
248
  // revalidation-contracts.ts
249
+ // Defer (|| undefined), not ?? false: a hard `false` short-circuits the chain,
250
+ // so when the same segment composes multiple contracts the later ones never run.
243
251
  export const revalidateCheckoutData = ({ actionId }) =>
244
- actionId?.includes("src/actions/checkout.ts#") ?? false;
252
+ actionId?.includes("src/actions/checkout.ts#") || undefined;
245
253
 
246
254
  path("/checkout", CheckoutPage, { name: "checkout" }, () => [
247
255
  revalidate(revalidateCheckoutData), // producer (route handler) reruns
@@ -270,9 +278,6 @@ path("/checkout", CheckoutPage, { name: "checkout" }, () => [
270
278
  ]);
271
279
  ```
272
280
 
273
- For scope/revalidation guarantees and non-guarantees, see:
274
- [docs/execution-model.md](../../docs/internal/execution-model.md)
275
-
276
281
  ## Redirects
277
282
 
278
283
  ### Basic redirect
@@ -403,6 +408,10 @@ urls(({ path, layout }) => [
403
408
  ])
404
409
  ```
405
410
 
411
+ ## View Transitions
412
+
413
+ A route can configure its own `transition()` — the wrap goes around the route's component itself (routes are leaves; they have no separate default outlet channel). If the route component renders a `<ParallelOutlet />` directly, that slot remains inside the route's VT subtree, so prefer mounting parallel slots in a layout when combining intercept modals with route-level transitions. See [skills/view-transitions](../view-transitions/SKILL.md) for examples and the wrap-location rules across layouts, routes, and slots.
414
+
406
415
  ## Handler-attached `.use`
407
416
 
408
417
  Page handlers can carry their own loader, middleware, error boundaries, parallels, and other defaults via a `.use` callback — so the page is self-contained and reusable across mount sites without re-wiring the same items.
@@ -71,7 +71,7 @@ urls(
71
71
  ## Router Options
72
72
 
73
73
  ```typescript
74
- interface RSCRouterOptions<TEnv> {
74
+ interface RangoOptions<TEnv> {
75
75
  // URL patterns from urls() function
76
76
  urls: UrlPatterns;
77
77
 
@@ -405,7 +405,7 @@ interface AppBindings {
405
405
  KV: KVNamespace;
406
406
  }
407
407
 
408
- // Variables declared via module augmentation
408
+ // Variables declared via global namespace augmentation
409
409
  interface AppVariables {
410
410
  user?: { id: string; name: string };
411
411
  }
@@ -417,7 +417,7 @@ const router = createRouter<AppBindings>({
417
417
 
418
418
  // Register types globally for implicit typing
419
419
  declare global {
420
- namespace RSCRouter {
420
+ namespace Rango {
421
421
  interface Env extends AppBindings {}
422
422
  interface Vars extends AppVariables {}
423
423
  }
@@ -32,35 +32,37 @@ Actions mutate state; route handlers and loaders read the latest state. After
32
32
  an action finishes, Rango performs a server-side revalidation render for the
33
33
  matched route so the UI receives fresh segment output and loader data.
34
34
 
35
- The main control point is `revalidate(({ actionId }) => ...)` on the segment
36
- that owns the data. This applies to `path()` handlers, `layout()` handlers,
37
- `parallel()` slots, `intercept()` routes, and loader registrations:
35
+ The main control point is `revalidate((ctx) => ...)` on the segment that owns
36
+ the data. Match specific actions by imported reference with `ctx.isAction()`;
37
+ use raw `actionId` only when you intentionally need path or directory matching.
38
+ This applies to `path()` handlers, `layout()` handlers, `parallel()` slots,
39
+ `intercept()` routes, and loader registrations:
38
40
 
39
41
  ```typescript
40
42
  // urls.tsx — path/layout/parallel/intercept/loader/revalidate are passed in by urls()
41
43
  import { urls } from "@rangojs/router";
44
+ import * as CartActions from "./actions/cart";
42
45
 
43
46
  export const urlpatterns = urls(({ path, loader, revalidate }) => [
44
47
  // The loader belongs to the route that consumes its data — nest it inside
45
48
  // the owning path() so the segment owns its data dependency.
46
49
  path("/cart", CartPage, { name: "cart" }, () => [
47
- revalidate(
48
- ({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
49
- ),
50
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
50
51
  loader(CartLoader, () => [
51
- revalidate(
52
- ({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
53
- ),
52
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
54
53
  ]),
55
54
  ]),
56
55
  ]);
57
56
  ```
58
57
 
59
- For module-level `"use server"` files, the `actionId` passed to every
58
+ `ctx.isAction()` resolves the imported action reference the same way the router
59
+ derives `actionId`, so it matches in both dev and production and survives action
60
+ renames/moves as type errors instead of silent substring drift.
61
+
62
+ For module-level `"use server"` files, the raw `actionId` passed to every
60
63
  server-side `revalidate()` predicate is path-bearing in the server/RSC
61
- environment in both dev and production: `src/actions/cart.ts#addToCart`. This
62
- is intentional so path, layout, parallel, intercept, and loader revalidation
63
- predicates can filter by action file, directory, or export name.
64
+ environment in both dev and production: `src/actions/cart.ts#addToCart`. This is
65
+ the escape hatch for broad filters by action file, directory, or export name.
64
66
 
65
67
  Actions and the follow-up revalidation render share one request context.
66
68
  Values written in the action with `ctx.set(MyVar, value)` or `ctx.set("key",
@@ -91,15 +93,18 @@ export async function switchTenant(tenantId: string) {
91
93
  ```typescript
92
94
  // urls.tsx
93
95
  import { urls } from "@rangojs/router";
96
+ import * as TenantActions from "./actions/tenant";
94
97
  import { ChangedTenant } from "./context";
95
98
 
96
99
  export const urlpatterns = urls(({ path, revalidate }) => [
97
100
  path("/dashboard/:tenantId", DashboardPage, { name: "dashboard" }, () => [
98
- revalidate(
99
- ({ actionId, context }) =>
100
- actionId?.startsWith("src/actions/tenant.ts#") &&
101
- context.get(ChangedTenant) === context.params.tenantId,
102
- ),
101
+ revalidate((ctx) => {
102
+ if (!ctx.isAction(TenantActions)) return undefined;
103
+ return (
104
+ ctx.context.get(ChangedTenant) === ctx.context.params.tenantId ||
105
+ undefined
106
+ );
107
+ }),
103
108
  ]),
104
109
  ]);
105
110
  ```
@@ -380,13 +385,18 @@ re-render so the UI updates. Rango runs the action, then evaluates
380
385
  `revalidate()` on matched segments and loaders. Each path, layout, parallel,
381
386
  intercept, or loader rule decides whether that piece re-renders/re-resolves.
382
387
 
383
- The `actionId` arrives as part of the revalidation context match it to
384
- scope re-runs to specific actions.
388
+ Use `ctx.isAction()` for specific actions or modules. It accepts one action,
389
+ several actions, or a namespace import (`import * as CartActions`). Pair it with
390
+ `|| undefined` for "revalidate on match, otherwise defer to defaults/downstream
391
+ rules."
385
392
 
386
393
  ```typescript
387
394
  // urls.tsx — inside the urls() callback. Nest each loader inside the path(),
388
395
  // layout(), or parallel() that owns its data so the route tree mirrors the
389
396
  // data dependencies.
397
+ import * as AccountActions from "./actions/account";
398
+ import * as CartActions from "./actions/cart";
399
+
390
400
  urls(({ path, loader, revalidate }) => [
391
401
  path("/", HomePage, { name: "home" }, () => [
392
402
  // Loader data re-runs by default after any action. Opt out with revalidate(() => false).
@@ -395,36 +405,37 @@ urls(({ path, loader, revalidate }) => [
395
405
 
396
406
  // Re-render the cart page handler AND re-resolve its loader after cart actions
397
407
  path("/cart", CartPage, { name: "cart" }, () => [
398
- revalidate(
399
- ({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
400
- ),
408
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
401
409
  loader(CartLoader, () => [
402
- revalidate(
403
- ({ actionId }) => actionId?.startsWith("src/actions/cart.ts#") ?? false,
404
- ),
410
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
405
411
  ]),
406
412
  ]),
407
413
 
408
- // Re-run after any action under src/actions/account/
414
+ // Re-run after any action exported by the account actions module
409
415
  path("/account", AccountPage, { name: "account" }, () => [
410
416
  loader(AccountLoader, () => [
411
- revalidate(
412
- ({ actionId }) => actionId?.startsWith("src/actions/account/") ?? false,
413
- ),
417
+ revalidate((ctx) => ctx.isAction(AccountActions) || undefined),
414
418
  ]),
415
419
  ]),
416
420
  ]);
417
421
  ```
418
422
 
419
- `actionId` is stable per action. For actions exported from a module-level
420
- `"use server"` file, the ID is prefixed with the source file path
421
- (`src/actions/cart.ts#addToCart`), so substring matching by file path is the
422
- recommended scope. **Inline `"use server"` actions** (declared inside an RSC
423
- component) intentionally keep their hashed IDs — file paths are withheld
424
- from the client for security. If you need file-path-based revalidation
425
- predicates, define the action in a module-level `"use server"` file rather
426
- than inline. See `/loader` for the full revalidation contract (deferred
427
- returns, soft suggestions).
423
+ The raw `actionId` string stays available for broad path filters:
424
+
425
+ ```typescript
426
+ // Match any action under src/actions/account/, including modules not imported here.
427
+ revalidate(
428
+ ({ actionId }) => actionId?.startsWith("src/actions/account/") || undefined,
429
+ );
430
+ ```
431
+
432
+ For actions exported from a module-level `"use server"` file, the ID is prefixed
433
+ with the source file path (`src/actions/cart.ts#addToCart`). **Inline `"use
434
+ server"` actions** (declared inside an RSC component) intentionally keep their
435
+ hashed IDs — file paths are withheld from the client for security. If you need
436
+ file-path-based revalidation predicates, define the action in a module-level
437
+ `"use server"` file rather than inline. See `/loader` for the full revalidation
438
+ contract (deferred returns, soft suggestions).
428
439
 
429
440
  ### Cross-segment dependencies
430
441
 
@@ -434,8 +445,9 @@ stale context. Share the same `revalidate` predicate on both producer and
434
445
  consumer:
435
446
 
436
447
  ```typescript
437
- const revalidateCart = ({ actionId }) =>
438
- actionId?.startsWith("src/actions/cart.ts#") ?? false;
448
+ import * as CartActions from "./actions/cart";
449
+
450
+ const revalidateCart = (ctx) => ctx.isAction(CartActions) || undefined;
439
451
 
440
452
  urls(({ path, layout, loader, revalidate }) => [
441
453
  layout(CartLayout, () => [