@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650

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 (356) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +71 -21
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +29 -5
  19. package/skills/layout/SKILL.md +13 -9
  20. package/skills/links/SKILL.md +173 -17
  21. package/skills/loader/SKILL.md +170 -23
  22. package/skills/middleware/SKILL.md +16 -10
  23. package/skills/migrate-nextjs/SKILL.md +38 -16
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +11 -7
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +250 -25
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +114 -47
  31. package/skills/route/SKILL.md +42 -5
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +78 -42
  34. package/skills/tailwind/SKILL.md +27 -3
  35. package/skills/testing/SKILL.md +129 -0
  36. package/skills/testing/bindings.md +89 -0
  37. package/skills/testing/cache-prerender.md +124 -0
  38. package/skills/testing/client-components.md +122 -0
  39. package/skills/testing/e2e-parity.md +125 -0
  40. package/skills/testing/flight.md +92 -0
  41. package/skills/testing/handles.md +129 -0
  42. package/skills/testing/loader.md +128 -0
  43. package/skills/testing/middleware.md +99 -0
  44. package/skills/testing/render-handler.md +121 -0
  45. package/skills/testing/response-routes.md +95 -0
  46. package/skills/testing/reverse-and-types.md +84 -0
  47. package/skills/testing/server-actions.md +107 -0
  48. package/skills/testing/server-tree.md +128 -0
  49. package/skills/testing/setup.md +120 -0
  50. package/skills/typesafety/SKILL.md +316 -26
  51. package/skills/use-cache/SKILL.md +36 -5
  52. package/skills/vercel/SKILL.md +107 -0
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/__internal.ts +0 -65
  57. package/src/browser/action-coordinator.ts +53 -36
  58. package/src/browser/action-fence.ts +47 -0
  59. package/src/browser/app-shell.ts +14 -27
  60. package/src/browser/cookie-name.ts +140 -0
  61. package/src/browser/event-controller.ts +37 -143
  62. package/src/browser/history-state.ts +21 -0
  63. package/src/browser/index.ts +3 -3
  64. package/src/browser/invalidate-client-cache.ts +52 -0
  65. package/src/browser/navigation-bridge.ts +30 -59
  66. package/src/browser/navigation-client.ts +96 -84
  67. package/src/browser/navigation-store-handle.ts +38 -0
  68. package/src/browser/navigation-store.ts +32 -82
  69. package/src/browser/navigation-transaction.ts +9 -59
  70. package/src/browser/partial-update.ts +60 -127
  71. package/src/browser/prefetch/cache.ts +82 -72
  72. package/src/browser/prefetch/fetch.ts +108 -33
  73. package/src/browser/prefetch/queue.ts +6 -3
  74. package/src/browser/rango-state.ts +157 -115
  75. package/src/browser/react/Link.tsx +0 -2
  76. package/src/browser/react/NavigationProvider.tsx +41 -48
  77. package/src/browser/react/ScrollRestoration.tsx +10 -6
  78. package/src/browser/react/filter-segment-order.ts +0 -2
  79. package/src/browser/react/index.ts +0 -48
  80. package/src/browser/react/location-state-shared.ts +166 -8
  81. package/src/browser/react/location-state.ts +39 -14
  82. package/src/browser/react/use-action.ts +6 -15
  83. package/src/browser/react/use-handle.ts +17 -14
  84. package/src/browser/react/use-link-status.ts +0 -4
  85. package/src/browser/react/use-navigation.ts +0 -3
  86. package/src/browser/react/use-params.ts +11 -11
  87. package/src/browser/react/use-reverse.ts +106 -0
  88. package/src/browser/react/use-router.ts +20 -5
  89. package/src/browser/react/use-search-params.ts +0 -5
  90. package/src/browser/react/use-segments.ts +0 -13
  91. package/src/browser/response-adapter.ts +52 -1
  92. package/src/browser/rsc-router.tsx +70 -34
  93. package/src/browser/scroll-restoration.ts +22 -14
  94. package/src/browser/segment-structure-assert.ts +2 -2
  95. package/src/browser/server-action-bridge.ts +168 -44
  96. package/src/browser/types.ts +36 -21
  97. package/src/browser/validate-redirect-origin.ts +43 -16
  98. package/src/build/collect-fallback-refs.ts +107 -0
  99. package/src/build/generate-manifest.ts +60 -35
  100. package/src/build/generate-route-types.ts +3 -0
  101. package/src/build/index.ts +8 -2
  102. package/src/build/prefix-tree-utils.ts +123 -0
  103. package/src/build/route-trie.ts +89 -10
  104. package/src/build/route-types/codegen.ts +4 -4
  105. package/src/build/route-types/include-resolution.ts +1 -1
  106. package/src/build/route-types/param-extraction.ts +6 -3
  107. package/src/build/route-types/per-module-writer.ts +7 -4
  108. package/src/build/route-types/router-processing.ts +122 -22
  109. package/src/build/route-types/scan-filter.ts +1 -1
  110. package/src/build/route-types/source-scan.ts +118 -0
  111. package/src/build/runtime-discovery.ts +9 -20
  112. package/src/cache/cache-error.ts +104 -0
  113. package/src/cache/cache-policy.ts +68 -28
  114. package/src/cache/cache-runtime.ts +134 -32
  115. package/src/cache/cache-scope.ts +100 -74
  116. package/src/cache/cache-tag.ts +98 -0
  117. package/src/cache/cf/cf-cache-store.ts +2255 -238
  118. package/src/cache/cf/index.ts +6 -16
  119. package/src/cache/document-cache.ts +61 -20
  120. package/src/cache/handle-snapshot.ts +63 -0
  121. package/src/cache/index.ts +22 -20
  122. package/src/cache/memory-segment-store.ts +136 -37
  123. package/src/cache/profile-registry.ts +6 -30
  124. package/src/cache/read-through-swr.ts +41 -11
  125. package/src/cache/segment-codec.ts +0 -16
  126. package/src/cache/tag-invalidation.ts +230 -0
  127. package/src/cache/types.ts +33 -100
  128. package/src/cache/vercel/index.ts +11 -0
  129. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  130. package/src/client.rsc.tsx +6 -21
  131. package/src/client.tsx +25 -61
  132. package/src/component-utils.ts +19 -0
  133. package/src/context-var.ts +17 -5
  134. package/src/decode-loader-results.ts +36 -0
  135. package/src/defer.ts +196 -0
  136. package/src/deps/ssr.ts +0 -1
  137. package/src/errors.ts +30 -4
  138. package/src/handle.ts +31 -23
  139. package/src/handles/MetaTags.tsx +0 -14
  140. package/src/handles/breadcrumbs.ts +16 -5
  141. package/src/handles/meta.ts +0 -39
  142. package/src/host/cookie-handler.ts +0 -36
  143. package/src/host/errors.ts +0 -24
  144. package/src/host/index.ts +8 -2
  145. package/src/host/pattern-matcher.ts +7 -50
  146. package/src/host/router.ts +107 -99
  147. package/src/host/testing.ts +40 -27
  148. package/src/host/types.ts +37 -4
  149. package/src/host/utils.ts +1 -1
  150. package/src/href-client.ts +137 -22
  151. package/src/index.rsc.ts +63 -9
  152. package/src/index.ts +64 -9
  153. package/src/internal-debug.ts +2 -4
  154. package/src/loader-store.ts +500 -0
  155. package/src/loader.rsc.ts +20 -13
  156. package/src/loader.ts +12 -11
  157. package/src/missing-id-error.ts +68 -0
  158. package/src/network-error-thrower.tsx +1 -6
  159. package/src/outlet-provider.tsx +1 -5
  160. package/src/prerender/param-hash.ts +10 -11
  161. package/src/prerender/store.ts +32 -37
  162. package/src/prerender.ts +61 -6
  163. package/src/redirect-origin.ts +100 -0
  164. package/src/response-utils.ts +9 -0
  165. package/src/reverse.ts +65 -40
  166. package/src/root-error-boundary.tsx +1 -19
  167. package/src/route-content-wrapper.tsx +7 -72
  168. package/src/route-definition/dsl-helpers.ts +244 -281
  169. package/src/route-definition/helper-factories.ts +29 -139
  170. package/src/route-definition/helpers-types.ts +40 -17
  171. package/src/route-definition/redirect.ts +43 -9
  172. package/src/route-definition/resolve-handler-use.ts +6 -0
  173. package/src/route-definition/use-item-types.ts +32 -0
  174. package/src/route-map-builder.ts +0 -16
  175. package/src/route-types.ts +19 -41
  176. package/src/router/basename.ts +14 -0
  177. package/src/router/content-negotiation.ts +15 -15
  178. package/src/router/error-handling.ts +13 -17
  179. package/src/router/find-match.ts +44 -23
  180. package/src/router/handler-context.ts +4 -41
  181. package/src/router/intercept-resolution.ts +14 -19
  182. package/src/router/lazy-includes.ts +9 -46
  183. package/src/router/loader-resolution.ts +91 -46
  184. package/src/router/logging.ts +0 -6
  185. package/src/router/manifest.ts +18 -29
  186. package/src/router/match-api.ts +0 -20
  187. package/src/router/match-context.ts +0 -22
  188. package/src/router/match-handlers.ts +57 -58
  189. package/src/router/match-middleware/background-revalidation.ts +0 -7
  190. package/src/router/match-middleware/cache-lookup.ts +150 -271
  191. package/src/router/match-middleware/cache-store.ts +3 -33
  192. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  193. package/src/router/match-middleware/segment-resolution.ts +0 -22
  194. package/src/router/match-pipelines.ts +1 -42
  195. package/src/router/match-result.ts +31 -80
  196. package/src/router/metrics.ts +0 -34
  197. package/src/router/middleware-types.ts +5 -112
  198. package/src/router/middleware.ts +118 -133
  199. package/src/router/navigation-snapshot.ts +0 -51
  200. package/src/router/params-util.ts +23 -0
  201. package/src/router/pattern-matching.ts +62 -67
  202. package/src/router/prerender-match.ts +99 -63
  203. package/src/router/preview-match.ts +3 -1
  204. package/src/router/request-classification.ts +28 -62
  205. package/src/router/revalidation.ts +50 -56
  206. package/src/router/route-snapshot.ts +0 -1
  207. package/src/router/router-context.ts +0 -27
  208. package/src/router/router-interfaces.ts +68 -35
  209. package/src/router/router-options.ts +55 -1
  210. package/src/router/router-registry.ts +2 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -63
  212. package/src/router/segment-resolution/helpers.ts +34 -0
  213. package/src/router/segment-resolution/loader-cache.ts +40 -37
  214. package/src/router/segment-resolution/revalidation.ts +203 -285
  215. package/src/router/segment-resolution/static-store.ts +19 -5
  216. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  217. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  218. package/src/router/segment-resolution.ts +4 -1
  219. package/src/router/segment-wrappers.ts +0 -3
  220. package/src/router/state-cookie-name.ts +33 -0
  221. package/src/router/substitute-pattern-params.ts +56 -0
  222. package/src/router/telemetry-otel.ts +0 -20
  223. package/src/router/telemetry.ts +96 -19
  224. package/src/router/timeout.ts +0 -20
  225. package/src/router/trie-matching.ts +87 -48
  226. package/src/router/types.ts +9 -63
  227. package/src/router/url-params.ts +0 -5
  228. package/src/router.ts +80 -41
  229. package/src/rsc/handler-context.ts +3 -2
  230. package/src/rsc/handler.ts +83 -78
  231. package/src/rsc/helpers.ts +93 -5
  232. package/src/rsc/index.ts +1 -1
  233. package/src/rsc/json-route-result.ts +38 -0
  234. package/src/rsc/manifest-init.ts +28 -41
  235. package/src/rsc/origin-guard.ts +39 -25
  236. package/src/rsc/progressive-enhancement.ts +12 -1
  237. package/src/rsc/redirect-guard.ts +99 -0
  238. package/src/rsc/response-error.ts +79 -12
  239. package/src/rsc/response-route-handler.ts +76 -62
  240. package/src/rsc/rsc-rendering.ts +41 -60
  241. package/src/rsc/runtime-warnings.ts +23 -10
  242. package/src/rsc/server-action.ts +62 -67
  243. package/src/rsc/ssr-setup.ts +16 -0
  244. package/src/rsc/types.ts +10 -5
  245. package/src/runtime-env.ts +18 -0
  246. package/src/search-params.ts +4 -20
  247. package/src/segment-loader-promise.ts +14 -2
  248. package/src/segment-system.tsx +199 -142
  249. package/src/serialize.ts +243 -0
  250. package/src/server/context.ts +150 -51
  251. package/src/server/cookie-store.ts +80 -5
  252. package/src/server/handle-store.ts +7 -24
  253. package/src/server/loader-registry.ts +5 -24
  254. package/src/server/request-context.ts +165 -87
  255. package/src/ssr/index.tsx +14 -14
  256. package/src/static-handler.ts +10 -13
  257. package/src/testing/cache-status.ts +162 -0
  258. package/src/testing/collect-handle.ts +40 -0
  259. package/src/testing/dispatch.ts +618 -0
  260. package/src/testing/dom.entry.ts +22 -0
  261. package/src/testing/e2e/fixture.ts +188 -0
  262. package/src/testing/e2e/index.ts +128 -0
  263. package/src/testing/e2e/matchers.ts +35 -0
  264. package/src/testing/e2e/page-helpers.ts +272 -0
  265. package/src/testing/e2e/parity.ts +387 -0
  266. package/src/testing/e2e/server.ts +195 -0
  267. package/src/testing/flight-matchers.ts +97 -0
  268. package/src/testing/flight-normalize.ts +11 -0
  269. package/src/testing/flight-runtime.d.ts +57 -0
  270. package/src/testing/flight-tree.ts +682 -0
  271. package/src/testing/flight.entry.ts +52 -0
  272. package/src/testing/flight.ts +232 -0
  273. package/src/testing/generated-routes.ts +183 -0
  274. package/src/testing/index.ts +99 -0
  275. package/src/testing/internal/context.ts +348 -0
  276. package/src/testing/internal/flight-client-globals.ts +30 -0
  277. package/src/testing/internal/seed-vars.ts +54 -0
  278. package/src/testing/render-handler.ts +330 -0
  279. package/src/testing/render-route.tsx +566 -0
  280. package/src/testing/run-loader.ts +378 -0
  281. package/src/testing/run-middleware.ts +205 -0
  282. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  283. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  284. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  285. package/src/testing/vitest-stubs/version.ts +5 -0
  286. package/src/testing/vitest.ts +305 -0
  287. package/src/theme/ThemeProvider.tsx +0 -52
  288. package/src/theme/ThemeScript.tsx +0 -6
  289. package/src/theme/constants.ts +0 -12
  290. package/src/theme/index.ts +0 -7
  291. package/src/theme/theme-context.ts +1 -5
  292. package/src/theme/theme-script.ts +0 -14
  293. package/src/theme/use-theme.ts +0 -3
  294. package/src/types/boundaries.ts +0 -35
  295. package/src/types/cache-types.ts +13 -4
  296. package/src/types/error-types.ts +30 -90
  297. package/src/types/global-namespace.ts +54 -41
  298. package/src/types/handler-context.ts +97 -22
  299. package/src/types/index.ts +1 -10
  300. package/src/types/loader-types.ts +6 -3
  301. package/src/types/request-scope.ts +0 -19
  302. package/src/types/route-config.ts +6 -50
  303. package/src/types/route-entry.ts +0 -6
  304. package/src/types/segments.ts +18 -14
  305. package/src/urls/include-helper.ts +9 -56
  306. package/src/urls/index.ts +1 -11
  307. package/src/urls/path-helper-types.ts +19 -5
  308. package/src/urls/path-helper.ts +17 -106
  309. package/src/urls/pattern-types.ts +36 -19
  310. package/src/urls/response-types.ts +20 -19
  311. package/src/urls/type-extraction.ts +58 -139
  312. package/src/urls/urls-function.ts +1 -18
  313. package/src/use-loader.tsx +292 -107
  314. package/src/vite/debug.ts +1 -0
  315. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  316. package/src/vite/discovery/discover-routers.ts +95 -82
  317. package/src/vite/discovery/discovery-errors.ts +194 -0
  318. package/src/vite/discovery/prerender-collection.ts +26 -34
  319. package/src/vite/discovery/route-types-writer.ts +40 -84
  320. package/src/vite/discovery/state.ts +39 -1
  321. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  322. package/src/vite/index.ts +4 -0
  323. package/src/vite/plugin-types.ts +185 -10
  324. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  325. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  326. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  327. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  328. package/src/vite/plugins/expose-action-id.ts +4 -75
  329. package/src/vite/plugins/expose-id-utils.ts +3 -54
  330. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  331. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  332. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  333. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  334. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  335. package/src/vite/plugins/performance-tracks.ts +9 -16
  336. package/src/vite/plugins/refresh-cmd.ts +1 -1
  337. package/src/vite/plugins/use-cache-transform.ts +26 -49
  338. package/src/vite/plugins/vercel-output.ts +258 -0
  339. package/src/vite/plugins/version-injector.ts +2 -32
  340. package/src/vite/plugins/version-plugin.ts +32 -23
  341. package/src/vite/plugins/virtual-entries.ts +35 -17
  342. package/src/vite/rango.ts +148 -115
  343. package/src/vite/router-discovery.ts +220 -68
  344. package/src/vite/utils/ast-handler-extract.ts +15 -31
  345. package/src/vite/utils/bundle-analysis.ts +10 -15
  346. package/src/vite/utils/client-chunks.ts +184 -0
  347. package/src/vite/utils/forward-user-plugins.ts +171 -0
  348. package/src/vite/utils/manifest-utils.ts +4 -59
  349. package/src/vite/utils/package-resolution.ts +1 -73
  350. package/src/vite/utils/prerender-utils.ts +0 -34
  351. package/src/vite/utils/shared-utils.ts +95 -43
  352. package/src/browser/action-response-classifier.ts +0 -99
  353. package/src/browser/react/use-client-cache.ts +0 -58
  354. package/src/browser/shallow.ts +0 -40
  355. package/src/handles/index.ts +0 -7
  356. package/src/router/middleware-cookies.ts +0 -55
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: links
3
- description: URL generation with ctx.reverse (server default), href (client), useHref (mounted), useMount, and scopedReverse
4
- argument-hint: [ctx.reverse|href|useHref|useMount|scopedReverse]
3
+ description: URL generation with ctx.reverse (server default), href (client), useHref (mounted), useMount, useReverse, and scopedReverse
4
+ argument-hint: [ctx.reverse|href|useHref|useMount|useReverse|scopedReverse]
5
5
  ---
6
6
 
7
7
  # Links & URL Generation
@@ -10,7 +10,12 @@ argument-hint: [ctx.reverse|href|useHref|useMount|scopedReverse]
10
10
 
11
11
  **Default server API: `ctx.reverse()`.** Generate URLs from the handler context — it's typed, auto-fills mount params, and resolves local (`.name`) and absolute (`name.sub`) names.
12
12
 
13
- **`reverse()` is server-only.** It depends on the route manifest and handler context, neither of which are available in the browser. Client components receive URLs as props, loader data, or server-action return values — they never call `reverse` directly.
13
+ **On the client, two patterns:**
14
+
15
+ 1. **Receive URLs as props / loader data / action return.** The default. The server has the full route manifest and handler context — generate URLs there and hand strings to client components.
16
+ 2. **`useReverse(routes)`.** Import a generated `routes` map from a `urls()` module's `.gen.ts` and call `reverse("name", params?)` (the leading dot is optional). Mount-aware via `useMount()`, auto-fills params from `useParams()`, fully typed from the imported map. Use this when a client component needs to generate URLs into a known module without round-tripping through the server.
17
+
18
+ `ctx.reverse()` itself is **server-only** — it depends on the full route manifest and handler context. Client components never import or call it.
14
19
 
15
20
  ## Server: ctx.reverse()
16
21
 
@@ -127,9 +132,7 @@ path("/product/:slug", (ctx) => {
127
132
 
128
133
  ## Client components: receive URLs as props
129
134
 
130
- `reverse()` is not available inside `"use client"` modules — there is no handler context and no route manifest in the browser bundle. Generate the URL on the server and hand it to the client component.
131
-
132
- Three patterns, in order of preference:
135
+ `ctx.reverse()` is not available inside `"use client"` modules — there is no handler context in the browser bundle. For in-module names, prefer `useReverse(routes)` (see below) and import the relevant `urls/*.gen.js`. For cross-module URLs or one-off names, generate the URL on the server and hand it to the client component using one of these three patterns:
133
136
 
134
137
  1. Pass as a prop from a server component:
135
138
 
@@ -210,7 +213,17 @@ function GlobalNav() {
210
213
  }
211
214
  ```
212
215
 
213
- `href()` provides compile-time validation via `ValidPaths` type. Paths are validated against registered route patterns using `PatternToPath`.
216
+ `href()` provides compile-time validation via the `Rango.Path` type. Paths are validated against registered route patterns using `PatternToPath`.
217
+
218
+ When wrapping `href()`, type the wrapper's path parameter as `Rango.Path` so it
219
+ keeps the same generated-route validation. `Rango.Path` is ambient — no import,
220
+ just like `Rango.Env` / `Rango.Vars`:
221
+
222
+ ```typescript
223
+ import { href } from "@rangojs/router/client";
224
+
225
+ export const appHref = (path: Rango.Path): string => href(path);
226
+ ```
214
227
 
215
228
  `href()` is a raw path helper — it is **not** basename-aware. It returns the path as-is (or with the include mount prefix via `useHref()`). For basename-aware navigation, use `Link`, `useRouter().push()`, or `reverse()`, which auto-prefix root-relative paths with the router's basename.
216
229
 
@@ -256,18 +269,161 @@ function MountInfo() {
256
269
 
257
270
  `useMount()` reads from `MountContext`, which is automatically set by `include()` in the segment tree.
258
271
 
259
- ## When to use what
272
+ ## Client: useReverse(routes)
273
+
274
+ Hook that returns a typed local reverse function for a `routes` map imported from a generated `.gen.ts` next to a `urls()` module. The route map is the **exposure boundary** — `useReverse` only knows about names in that map, never the full app manifest.
275
+
276
+ > **Which map?** `useReverse` accepts any routes map. Prefer the per-module `routes` (e.g. `urls/blog.gen.ts`): it gives **mount-aware** local `.name` reverse (auto-prefixes the `include()` mount) and only that module's names enter the client bundle. You _can_ instead pass `router.named-routes.gen.ts` (`NamedRoutes`) for **global** names (`blog.post`; the leading dot is optional) — it is a plain importable map and works on the client (it is **not** server-only) — but its paths are **absolute** while `useReverse` mount-prefixes, so it is correct only at the root mount (under a non-root mount it double-prefixes), and importing it pulls every route name and pattern in the app into the client bundle (a small names-to-paths map — not components or loaders), versus the per-module map which exposes only one module's names. So the per-module map is preferred for in-module links; the named-routes map is the escape hatch for global names.
277
+
278
+ ```tsx
279
+ "use client";
280
+ import { Link, useReverse } from "@rangojs/router/client";
281
+ import { routes as blogRoutes } from "../urls/blog.gen.js";
282
+
283
+ export function BlogNav() {
284
+ const reverse = useReverse(blogRoutes);
285
+
286
+ return (
287
+ <nav>
288
+ <Link to={reverse("index")}>Blog</Link>
289
+ <Link to={reverse("post", { postId: "hello" })}>Post</Link>
290
+ </nav>
291
+ );
292
+ }
293
+ ```
294
+
295
+ ### How it resolves
296
+
297
+ 1. Strips an optional leading `.` and looks up the name in the imported `routes` map.
298
+ 2. Joins the local pattern with the surrounding `useMount()` value — the include's URL pattern.
299
+ 3. Substitutes params: explicit params from the call, then auto-filled from `useParams()` for anything still unresolved (mount params like `:tenantId` flow in this way).
300
+ 4. Appends a query string if a search object is passed and the route has a `search` schema.
301
+
302
+ ### Mount-relativity
303
+
304
+ Patterns in the generated `routes` map are **mount-relative** — they're the patterns as defined inside the `urls()` module, _not_ the full app paths. Mount-joining happens at runtime via `useMount()`, so the same component works under any include:
305
+
306
+ ```typescript
307
+ // urls/blog.tsx
308
+ export const blogPatterns = urls(({ path }) => [
309
+ path("/", BlogIndex, { name: "index" }),
310
+ path("/:postId", BlogPost, { name: "post" }),
311
+ ]);
312
+
313
+ // Generated urls/blog.gen.ts
314
+ // export const routes = { index: "/", post: "/:postId" } as const;
315
+
316
+ // urls.tsx — same module mounted twice
317
+ include("/news", blogPatterns, { name: "news" }), // <BlogNav> renders /news, /news/hello
318
+ include("/journal", blogPatterns, { name: "diary" }), // <BlogNav> renders /journal, /journal/hello
319
+ ```
320
+
321
+ The `/` pattern under a non-root mount collapses cleanly: under `/news`, `reverse(".index")` returns `/news` (no trailing slash), matching `ctx.reverse(".index")` on the server.
322
+
323
+ ### Auto-filled params (mount params)
324
+
325
+ When the include itself carries `:params`, those are auto-filled from `useParams()` so the caller doesn't have to thread them through:
326
+
327
+ ```typescript
328
+ // urls.tsx
329
+ include("/tenant/:tenantId", clientReversePatterns, { name: "tenant" });
330
+ ```
260
331
 
261
- | Context | API | Resolves | Use for |
262
- | ---------------- | -------------------------------------------------- | ------------------------------- | ---------------------------------------------------------------- |
263
- | Server handler | `ctx.reverse("name")` | Named routes (local + absolute) | **Default** server-side URL generation |
264
- | Server handler | `scopedReverse<T>(ctx.reverse)` | Same, with type safety | Type-safe server URLs |
265
- | Client component | (URL passed as prop / loader data / action return) | Named routes | Any URL derived from a named route — generate on server, pass in |
266
- | Client component | `href("/path")` | Absolute paths (static strings) | Static navigation where no named-route lookup is needed |
267
- | Client component | `useHref()` | Mount-prefixed paths | Local navigation inside `include()` |
268
- | Client component | `useMount()` | Raw mount path | Custom mount-aware logic |
332
+ ```tsx
333
+ // At /tenant/acme/posts/p1, useParams() = { tenantId: "acme", postId: "p1" }
334
+ const reverse = useReverse(clientReverseRoutes);
335
+
336
+ reverse(".index"); // "/tenant/acme"
337
+ reverse(".post", { postId: "p2" }); // "/tenant/acme/posts/p2" (tenantId auto-filled)
338
+ reverse(".post", { tenantId: "other", postId: "p2" }); // "/tenant/other/posts/p2" (explicit override)
339
+ ```
340
+
341
+ Auto-fill follows soft navigation — when the matched route changes, `useReverse` re-renders with the new params.
342
+
343
+ ### Search schemas
344
+
345
+ Routes declared with a `search` schema accept a typed search object as the third argument:
346
+
347
+ ```typescript
348
+ // urls/blog.tsx
349
+ path("/search", SearchPage, {
350
+ name: "search",
351
+ search: { q: "string", page: "number?" },
352
+ }),
353
+
354
+ // Generated as: search: { path: "/search", search: { q: "string", page: "number?" } }
355
+ ```
356
+
357
+ ```tsx
358
+ const reverse = useReverse(blogRoutes);
359
+ reverse(".search", {}, { q: "hello world", page: 2 });
360
+ // "/news/search?q=hello%20world&page=2"
361
+ ```
362
+
363
+ ### Errors
364
+
365
+ - Unknown name: throws `Unknown route: ".not-a-route"`.
366
+ - Missing required param: throws `Missing param "postId" for route ".detail"`.
367
+
368
+ Both happen synchronously during `reverse()` — wrap calls in try/catch (or an ErrorBoundary if the throw happens during render) when you need to surface them as UI.
369
+
370
+ ### The leading dot is optional
371
+
372
+ `reverse("post")` and `reverse(".post")` resolve **identically** — the leading dot is cosmetic. The map you import IS the scope, so there is no separate global namespace to disambiguate and the dot carries no meaning; it exists only as a readability convention and for parity with `ctx.reverse(".name")` on the server. To link into a different module, import that module's `routes`:
373
+
374
+ ```tsx
375
+ import { routes as blogRoutes } from "../urls/blog.gen.js";
376
+ import { routes as shopRoutes } from "../urls/shop.gen.js";
377
+
378
+ function CrossNav() {
379
+ const blog = useReverse(blogRoutes);
380
+ const shop = useReverse(shopRoutes);
381
+ return (
382
+ <nav>
383
+ <Link to={blog("index")}>Blog</Link>
384
+ <Link to={shop("cart")}>Cart</Link>
385
+ </nav>
386
+ );
387
+ }
388
+ ```
389
+
390
+ ### Codegen
391
+
392
+ Each `urls()` module gets a sibling `.gen.ts` with the local route names and patterns, produced by `rango generate`:
393
+
394
+ ```bash
395
+ pnpm exec rango generate src/urls/blog.tsx
396
+ # or generate everything under a directory:
397
+ pnpm exec rango generate src/urls --static
398
+ ```
399
+
400
+ Don't edit the file by hand — re-run codegen when patterns change.
401
+
402
+ **Today the Vite plugin only regenerates the router-level `*.named-routes.gen.ts`.** Per-module `urls/*.gen.ts` files are emitted only by the CLI (or `writePerModuleRouteTypesForFile` programmatically). Commit the generated files and re-run `rango generate` whenever a `urls()` module's `path()`/`include()` shape changes. A common workflow is to wire it into a `predev` script:
403
+
404
+ ```jsonc
405
+ // package.json
406
+ {
407
+ "scripts": {
408
+ "predev": "rango generate src",
409
+ "dev": "vite",
410
+ },
411
+ }
412
+ ```
413
+
414
+ ## When to use what
269
415
 
270
- > `reverse()` is server-only. Client components never import or call it — they receive the already-resolved string.
416
+ | Context | API | Resolves | Use for |
417
+ | ---------------- | -------------------------------------------------- | ----------------------------------------- | ---------------------------------------------------------------- |
418
+ | Server handler | `ctx.reverse("name")` | Named routes (local + absolute) | **Default** server-side URL generation |
419
+ | Server handler | `scopedReverse<T>(ctx.reverse)` | Same, with type safety | Type-safe server URLs |
420
+ | Client component | `useReverse(routes)` | Local names from an imported `routes` map | Typed in-module URL generation without round-tripping the server |
421
+ | Client component | (URL passed as prop / loader data / action return) | Named routes | Cross-module URLs or one-off names you don't want to import |
422
+ | Client component | `href("/path")` | Absolute paths (static strings) | Static navigation where no named-route lookup is needed |
423
+ | Client component | `useHref()` | Mount-prefixed paths | Local navigation inside `include()` |
424
+ | Client component | `useMount()` | Raw mount path | Custom mount-aware logic |
425
+
426
+ > `ctx.reverse()` is server-only. On the client, either generate URLs on the server and pass them in, or import the `routes` map and use `useReverse(routes)` for in-module names.
271
427
 
272
428
  ## Complete example: mounted module
273
429
 
@@ -91,6 +91,20 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
91
91
  ]);
92
92
  ```
93
93
 
94
+ > **Client refresh `key` vs. server `cache({ key })` vs. `revalidate()`.** Three
95
+ > different "what refreshes" knobs that are easy to confuse:
96
+ >
97
+ > - `useLoader(Loader, { key })` / `useFetchLoader(Loader, { key })` — a
98
+ > **client** refresh identity. It groups which mounted reads of one loader
99
+ > refresh together when one calls `load()`. It never touches the server
100
+ > request. For refreshing **different** loaders together, tag them with
101
+ > `{ refreshGroup }` (one name or several) and call `useRefreshLoaders()(name)`
102
+ > (plain GET only). See the hooks skill ("Scoping refetch with a `key`" and
103
+ > "Refreshing multiple loaders together").
104
+ > - `cache({ key })` — a **server** cache identity (storage hit/miss/ttl/swr).
105
+ > - `revalidate()` — which **server** segments/loaders recompute during
106
+ > navigation and action refreshes.
107
+
94
108
  DSL loaders are the **live data layer** — they resolve fresh on every
95
109
  request, even when the route is inside a `cache()` boundary. The router
96
110
  excludes them from the segment cache at storage time and re-resolves them
@@ -146,23 +160,23 @@ Loaders receive the same context shape as route handlers.
146
160
 
147
161
  ### Full field surface
148
162
 
149
- | Field | Type | Notes |
150
- | -------------- | ------------------------------ | --------------------------------------------------------------------------------------------------- |
151
- | `params` | `TParams` | Merged route + explicit loader params; overridable by fetchable `load({ params })`. |
152
- | `routeParams` | `Record<string, string>` | Server-trusted route params from URL pattern matching; cannot be overridden. |
153
- | `request` | `Request` | The incoming `Request` (headers, method, body, `signal` for abort). |
154
- | `url` | `URL` | Parsed request URL. |
155
- | `pathname` | `string` | URL pathname (shortcut for `ctx.url.pathname`). |
156
- | `searchParams` | `URLSearchParams` | Shortcut for `ctx.url.searchParams`. |
157
- | `search` | `ResolveSearchSchema<TSearch>` | Typed query params when a search schema is declared on the route; `{}` otherwise. |
158
- | `env` | `TEnv` | Plain bindings from `createRouter<TEnv>()` (DB, KV, secrets, etc.). |
159
- | `get` | `(key \| ContextVar) => value` | Reads variables/context-vars set by middleware. |
160
- | `use` | `(loader \| handle) => T` | Access another loader's data (Promise) or a handle's collected data (after `await ctx.rendered()`). |
161
- | `rendered` | `() => Promise<void>` | **Experimental.** DSL loaders only — waits for non-loader segments before reading handle data. |
162
- | `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders. |
163
- | `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls. |
164
- | `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission. |
165
- | `reverse` | `ScopedReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers). |
163
+ | Field | Type | Notes |
164
+ | -------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
165
+ | `params` | `TParams` | Merged route + explicit loader params; overridable by fetchable `load({ params })`. |
166
+ | `routeParams` | `Record<string, string>` | Server-trusted route params from URL pattern matching; cannot be overridden. |
167
+ | `request` | `Request` | The incoming `Request` (headers, method, body, `signal` for abort). |
168
+ | `url` | `URL` | Parsed request URL. |
169
+ | `pathname` | `string` | URL pathname (shortcut for `ctx.url.pathname`). |
170
+ | `searchParams` | `URLSearchParams` | Shortcut for `ctx.url.searchParams`. |
171
+ | `search` | `ResolveSearchSchema<TSearch>` | Typed query params when a search schema is declared on the route; `{}` otherwise. |
172
+ | `env` | `TEnv` | Plain bindings from `createRouter<TEnv>()` (DB, KV, secrets, etc.). |
173
+ | `get` | `(key \| ContextVar) => value` | Reads variables/context-vars set by middleware. |
174
+ | `use` | `(loader \| handle) => T` | Access another loader's data (Promise) or a handle's collected data (after `await ctx.rendered()`). |
175
+ | `rendered` | `() => Promise<void>` | **Experimental.** DSL loaders only — waits for all non-loader segments (including `loading()` streaming handlers) to settle before reading handle data. |
176
+ | `method` | `string` | HTTP method. `"GET"` for SSR loader runs; reflects real method for fetchable loaders. |
177
+ | `body` | `TBody \| undefined` | Parsed request body for fetchable POST/PUT/PATCH/DELETE calls. |
178
+ | `formData` | `FormData \| undefined` | Present when a fetchable loader is invoked via form submission. |
179
+ | `reverse` | `ScopedReverseFunction` | Generate type-checked URLs from route names (same scoped semantics as route handlers). |
166
180
 
167
181
  ### Example
168
182
 
@@ -185,7 +199,7 @@ export const ProductLoader = createLoader(async (ctx) => {
185
199
  // Request headers
186
200
  const auth = ctx.request.headers.get("Authorization");
187
201
 
188
- // Variables set by middleware (from RSCRouter.Vars augmentation)
202
+ // Variables set by middleware (from Rango.Vars augmentation)
189
203
  const user = ctx.get("user");
190
204
 
191
205
  // Type-checked URLs for payloads. `.name` resolves within the current
@@ -235,6 +249,8 @@ export const OrderLoader = createLoader(async (ctx) => {
235
249
  Add caching or revalidation to specific loaders:
236
250
 
237
251
  ```typescript
252
+ import * as CartActions from "./actions/cart";
253
+
238
254
  path("/product/:slug", ProductPage, { name: "product" }, () => [
239
255
  // Cached loader
240
256
  loader(ProductLoader, () => [cache({ ttl: 300 })]),
@@ -244,15 +260,23 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
244
260
  revalidate(() => false), // Never revalidate
245
261
  ]),
246
262
 
247
- // Loader that revalidates after cart actions
263
+ // Loader that revalidates after cart actions (defer otherwise — keeps the
264
+ // permissive loader defaults for navigation and other actions intact)
248
265
  loader(CartLoader, () => [
249
- revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
266
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
250
267
  ]),
251
268
  ]);
252
269
  ```
253
270
 
254
271
  ### `revalidate()` return shapes
255
272
 
273
+ > **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
274
+ > It decides whether a segment (here, a loader) re-runs and streams to the
275
+ > client on a navigation or action — never whether a cached value is stale. The
276
+ > cache decides hit/miss/ttl/swr independently and never reads `revalidate()`.
277
+ > Caching a loader is a separate, opt-in step (`loader(Fn, () => [cache({...})])`).
278
+ > See `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
279
+
256
280
  A `revalidate(fn)` callback can return one of four shapes. The chain
257
281
  processes revalidators in order; each call's return controls how the
258
282
  chain continues:
@@ -282,6 +306,58 @@ revalidate(() => null); // explicit defer
282
306
  If every revalidator on a segment defers, the segment-type default
283
307
  (e.g. params-changed for routes, `false` for parallels) is used.
284
308
 
309
+ #### `|| undefined` (defer) vs `?? false` (hard) — pick deliberately
310
+
311
+ A boolean return — including `false` — is a **hard** decision: it short-circuits
312
+ the chain and overrides the segment default. `undefined` **defers** to the
313
+ running suggestion / segment default. They are not interchangeable:
314
+
315
+ ```typescript
316
+ // Defer: "revalidate on match, otherwise let the default/downstream decide."
317
+ revalidate(({ actionId }) => actionId?.includes("Cart") || undefined);
318
+
319
+ // Hard: "revalidate ONLY on match, suppress everything else."
320
+ revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
321
+ ```
322
+
323
+ This matters most for loaders, whose defaults are permissive: a loader defaults
324
+ to revalidating on **any** action (`POST`) and on **param/search changes**
325
+ during navigation. So `?? false` on a loader silently suppresses both — the
326
+ loader will not refetch when you navigate to a different `:id`. Use
327
+ `|| undefined` when you want to _add_ a revalidation signal on top of the
328
+ sensible defaults, and reserve `?? false` for the rare case where you genuinely
329
+ want the loader to refetch on nothing but your matched action.
330
+
331
+ When **composing multiple revalidators** on one segment (see below), defer is
332
+ mandatory: the first hard `?? false` ends the chain and the later contracts
333
+ never run.
334
+
335
+ #### Matching actions: `ctx.isAction()`
336
+
337
+ To revalidate after specific server actions, match them by **reference** with
338
+ `ctx.isAction()` rather than hand-written `actionId` substrings. A rename or
339
+ moved file then becomes a type error instead of silently failing to match:
340
+
341
+ ```typescript
342
+ import { addToCart, removeFromCart } from "../actions/cart";
343
+ import * as CartActions from "../actions/cart";
344
+
345
+ loader(CartLoader, () => [
346
+ revalidate((ctx) => ctx.isAction(addToCart) || undefined), // one action
347
+ ]);
348
+ revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
349
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any action in the module
350
+ ```
351
+
352
+ `isAction()` is a method on the revalidate predicate's **context argument** —
353
+ there is no standalone `isAction` import; you always reach it through the callback
354
+ parameter (`revalidate((ctx) => ctx.isAction(...))`). It returns a raw boolean, so
355
+ pair it with `|| undefined` for the usual "revalidate on match, else defer"
356
+ intent. It returns `false` on plain navigation and on non-matches, and resolves
357
+ the reference the same way the router derives `actionId` (`$id` in production,
358
+ `$$id` in dev), so it matches in both modes. The raw `actionId` string stays
359
+ available on the same context as an escape hatch.
360
+
285
361
  ### Revalidation Contracts for Loader Dependencies
286
362
 
287
363
  If a loader reads `ctx.get()` data produced by an outer handler/layout, share
@@ -289,8 +365,12 @@ the same named revalidation contract across producer and consumer segments.
289
365
 
290
366
  ```typescript
291
367
  // revalidation-contracts.ts
292
- export const revalidateAccountScope = ({ actionId }) =>
293
- actionId?.includes("src/actions/account.ts#") ?? false;
368
+ import * as AccountActions from "./actions/account";
369
+
370
+ // Match by reference with ctx.isAction() (rename-safe), and defer (|| undefined)
371
+ // so these contracts compose — a hard `false` would short-circuit the rest.
372
+ export const revalidateAccountScope = (ctx) =>
373
+ ctx.isAction(AccountActions) || undefined;
294
374
 
295
375
  layout(AccountLayout, () => [
296
376
  revalidate(revalidateAccountScope), // producer reruns
@@ -333,6 +413,64 @@ follows the same rule: at build time, loaders are skipped entirely (there is no
333
413
  real request context), and at runtime the worker resolves them fresh against
334
414
  the live database.
335
415
 
416
+ ### Parallel and streaming — latency overlaps first paint
417
+
418
+ Loaders do not block the page. As the render pass begins — the pass that route
419
+ middleware wraps, so loaders run right after middleware, not in a later
420
+ phase — every matched loader is kicked off **concurrently** (their promises start in the
421
+ same tick), and each result is **streamed** to the client as its own RSC Flight
422
+ chunk rather than awaited up front. Pair a loader with `loading()` (or a
423
+ client `<Suspense>`) and the shell paints immediately while the data streams in.
424
+
425
+ This is why **"cached UI still pays full data latency" is the wrong intuition**:
426
+ on a `cache()` hit the UI segments stream instantly from cache while the live
427
+ loaders resolve fresh **in parallel** — data latency _overlaps_ first paint
428
+ instead of being added on top of it. (Without a `loading()` / `<Suspense>`
429
+ boundary a parallel loader blocks its parent, so add one to keep the overlap.)
430
+
431
+ If you come from a framework where the loader is a blocking step that runs
432
+ before the response is built, this is the shift to internalize: here the
433
+ response starts streaming first and loader data fills in.
434
+
435
+ ### See it: `debugPerformance`
436
+
437
+ Turn on the per-request performance timeline early — it is the fastest way to
438
+ confirm loaders overlap rather than serialize, and to find the real bottleneck
439
+ locally instead of guessing:
440
+
441
+ ```typescript
442
+ const router = createRouter({ document: Document, debugPerformance: true });
443
+ ```
444
+
445
+ Or enable it per-request from middleware (e.g. only when `?debug` is present) by
446
+ calling `ctx.debugPerformance()` **before** `await next()`. Each HTML request
447
+ then prints a shared-axis waterfall (and emits a `Server-Timing` header):
448
+
449
+ ```
450
+ [RSC Perf] GET /product/widget (24.53ms)
451
+ start dur span timeline
452
+ 0.08ms 3.20ms route-matching |#####...................................|
453
+ 3.40ms 8.70ms ssr-render-html |.....##############.....................|
454
+ 3.42ms 11.90ms loader:…#ProductLoader |.....###################................|
455
+ 3.45ms 11.40ms loader:…#ReviewsLoader |.....##################.................|
456
+ 0.00ms 24.53ms handler:total |########################################|
457
+ ```
458
+
459
+ How to read it:
460
+
461
+ - **Humans:** scan the `#` bars on the shared axis. Bars that start at the same
462
+ offset and run side by side are executing **in parallel** — loaders should
463
+ overlap `ssr-render-html` / `render:total`, not sit alone to the right of
464
+ everything. A lone `loader:*` bar past the render bar is serialized latency to
465
+ chase. `handler:total` is the whole request; `render:total` is the render pass.
466
+ - **LLMs / programmatic:** read each row as `{ start, dur, label }`. A loader
467
+ overlaps paint when its `[start, start+dur]` interval intersects
468
+ `render:total` / `ssr-render-html`. Flag a regression when a `loader:*`
469
+ interval is **disjoint from and starts after** `render:total`, or when its
470
+ `dur` approaches `handler:total` — that loader is on the critical path instead
471
+ of overlapping it. Two `loader:*` rows with near-equal `start` confirm
472
+ parallel execution.
473
+
336
474
  ### Opting a Loader into Caching
337
475
 
338
476
  To cache a specific loader's data, attach a `cache()` child:
@@ -606,6 +744,13 @@ export const FileUploadLoader = createLoader(async (ctx) => {
606
744
 
607
745
  Client usage — see `/hooks useFetchLoader` for the full client-side pattern.
608
746
 
747
+ > **Refetch sharing**: when the loader is registered on the route via
748
+ > `loader()`, a plain `load()` call (no `params`, no `body`) broadcasts
749
+ > the new value to every component reading the same loader id —
750
+ > `useLoader` reads in layouts, pages, and parallel slots all converge.
751
+ > Calls with `params` or a non-GET method stay local to the call site.
752
+ > See `/hooks` → "Shared refetch behavior" for the full contract.
753
+
609
754
  ## Complete Example
610
755
 
611
756
  ```typescript
@@ -638,10 +783,12 @@ export const CartLoader = createLoader(async (ctx) => {
638
783
  });
639
784
 
640
785
  // urls.tsx — register loaders in the DSL
786
+ import * as CartActions from "./actions/cart";
787
+
641
788
  export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
642
789
  layout(<ShopLayout />, () => [
643
790
  loader(CartLoader, () => [
644
- revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
791
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
645
792
  ]),
646
793
 
647
794
  path("/shop/product/:slug", ProductPage, { name: "product" }, () => [
@@ -10,9 +10,6 @@ Middleware runs before/after route handlers using the onion model.
10
10
 
11
11
  ## Execution Model
12
12
 
13
- Canonical semantics reference:
14
- [docs/execution-model.md](../../docs/internal/execution-model.md)
15
-
16
13
  There are two levels of middleware with different execution scopes:
17
14
 
18
15
  ### Global middleware (`router.use()`)
@@ -36,15 +33,22 @@ Registered inside `urls()` callback. Wraps **rendering only** -- it does NOT wra
36
33
 
37
34
  ```
38
35
  Request flow (with action):
39
- global mw -> action executes -> route mw -> layout -> handler -> loaders
36
+ global mw -> action executes -> route mw -> render pass
40
37
 
41
38
  Request flow (no action):
42
- global mw -> route mw -> layout -> handler -> loaders
39
+ global mw -> route mw -> render pass
43
40
 
44
41
  Progressive enhancement (no-JS form POST):
45
42
  global mw -> action executes -> route mw -> full page re-render
46
43
  ```
47
44
 
45
+ The **render pass** resolves handler, layouts, parallels, and loaders together —
46
+ it is not a handler-then-loaders sequence. Handler-first ordering is guaranteed
47
+ only between a route handler and its child/orphan layouts and parallels (so
48
+ `ctx.set` is visible); loaders run **concurrently** and stream their results, so
49
+ their latency overlaps rendering rather than blocking it. See `/loader` →
50
+ "Parallel and streaming".
51
+
48
52
  The contract is: **route middleware wraps rendering regardless of transport** (JS-enabled RSC stream or no-JS HTML). During PE re-render, route middleware observes action-set state (cookies, context variables) the same way it does during JS-enabled post-action revalidation.
49
53
 
50
54
  Revalidation is still partial. Route middleware wraps the render pass that
@@ -63,8 +67,10 @@ For shared segment data, use named revalidation contracts on both the producer
63
67
  and consumer segments, even when middleware is present in the chain.
64
68
 
65
69
  ```typescript
66
- export const revalidateCartData = ({ actionId }) =>
67
- actionId?.includes("src/actions/cart.ts#") ?? false;
70
+ import * as CartActions from "./actions/cart";
71
+
72
+ export const revalidateCartData = (ctx) =>
73
+ ctx.isAction(CartActions) || undefined;
68
74
 
69
75
  layout(CartLayout, () => [
70
76
  middleware(cartRenderMiddleware),
@@ -192,7 +198,7 @@ export const myMiddleware: Middleware = async (ctx, next) => {
192
198
  ctx.env.DB; // D1Database
193
199
  ctx.env.KV; // KVNamespace
194
200
 
195
- // Set variables for downstream handlers (typed via RSCRouter.Vars)
201
+ // Set variables for downstream handlers (typed via Rango.Vars)
196
202
  ctx.set("user", { id: "123", name: "John" });
197
203
 
198
204
  // Continue to next middleware/handler
@@ -233,8 +239,8 @@ const Dashboard: Handler<"dashboard"> = (ctx) => {
233
239
  ```
234
240
 
235
241
  This works alongside `ctx.get("key")` / `ctx.set("key", value)` (global typing
236
- via RSCRouter.Vars augmentation). Use `createVar` for route-local or feature-scoped
237
- data; use RSCRouter.Vars for app-wide middleware state.
242
+ via Rango.Vars augmentation). Use `createVar` for route-local or feature-scoped
243
+ data; use Rango.Vars for app-wide middleware state.
238
244
 
239
245
  ## Redirect with State in Middleware
240
246