@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.d98a8e9d

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 (278) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +2154 -861
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +57 -11
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +3 -1
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +220 -30
  11. package/skills/caching/SKILL.md +116 -8
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +45 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +247 -17
  21. package/skills/loader/SKILL.md +219 -9
  22. package/skills/middleware/SKILL.md +47 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +71 -6
  28. package/skills/prerender/SKILL.md +14 -33
  29. package/skills/rango/SKILL.md +243 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +122 -47
  32. package/skills/route/SKILL.md +57 -4
  33. package/skills/router-setup/SKILL.md +3 -3
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +128 -0
  37. package/skills/testing/bindings.md +89 -0
  38. package/skills/testing/cache-prerender.md +98 -0
  39. package/skills/testing/client-components.md +121 -0
  40. package/skills/testing/e2e-parity.md +124 -0
  41. package/skills/testing/flight.md +89 -0
  42. package/skills/testing/handles.md +127 -0
  43. package/skills/testing/loader.md +108 -0
  44. package/skills/testing/middleware.md +97 -0
  45. package/skills/testing/render-handler.md +102 -0
  46. package/skills/testing/response-routes.md +94 -0
  47. package/skills/testing/reverse-and-types.md +83 -0
  48. package/skills/testing/server-actions.md +89 -0
  49. package/skills/testing/server-tree.md +128 -0
  50. package/skills/testing/setup.md +120 -0
  51. package/skills/typesafety/SKILL.md +319 -27
  52. package/skills/use-cache/SKILL.md +34 -5
  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/browser/action-coordinator.ts +53 -36
  57. package/src/browser/app-shell.ts +52 -0
  58. package/src/browser/event-controller.ts +86 -70
  59. package/src/browser/history-state.ts +21 -0
  60. package/src/browser/index.ts +3 -3
  61. package/src/browser/navigation-bridge.ts +84 -11
  62. package/src/browser/navigation-client.ts +104 -68
  63. package/src/browser/navigation-store.ts +32 -9
  64. package/src/browser/navigation-transaction.ts +10 -28
  65. package/src/browser/partial-update.ts +64 -26
  66. package/src/browser/prefetch/cache.ts +183 -44
  67. package/src/browser/prefetch/fetch.ts +228 -37
  68. package/src/browser/prefetch/queue.ts +36 -5
  69. package/src/browser/rango-state.ts +53 -13
  70. package/src/browser/react/Link.tsx +30 -2
  71. package/src/browser/react/NavigationProvider.tsx +72 -31
  72. package/src/browser/react/filter-segment-order.ts +51 -7
  73. package/src/browser/react/index.ts +3 -0
  74. package/src/browser/react/location-state-shared.ts +175 -4
  75. package/src/browser/react/location-state.ts +39 -13
  76. package/src/browser/react/use-handle.ts +17 -9
  77. package/src/browser/react/use-navigation.ts +22 -2
  78. package/src/browser/react/use-params.ts +20 -8
  79. package/src/browser/react/use-reverse.ts +106 -0
  80. package/src/browser/react/use-router.ts +22 -2
  81. package/src/browser/react/use-segments.ts +11 -8
  82. package/src/browser/response-adapter.ts +32 -1
  83. package/src/browser/rsc-router.tsx +69 -22
  84. package/src/browser/scroll-restoration.ts +22 -14
  85. package/src/browser/segment-reconciler.ts +36 -14
  86. package/src/browser/segment-structure-assert.ts +2 -2
  87. package/src/browser/server-action-bridge.ts +23 -30
  88. package/src/browser/types.ts +21 -0
  89. package/src/build/collect-fallback-refs.ts +107 -0
  90. package/src/build/generate-manifest.ts +60 -35
  91. package/src/build/generate-route-types.ts +2 -0
  92. package/src/build/index.ts +8 -1
  93. package/src/build/prefix-tree-utils.ts +123 -0
  94. package/src/build/route-trie.ts +95 -25
  95. package/src/build/route-types/codegen.ts +4 -4
  96. package/src/build/route-types/include-resolution.ts +1 -1
  97. package/src/build/route-types/per-module-writer.ts +7 -4
  98. package/src/build/route-types/router-processing.ts +55 -14
  99. package/src/build/route-types/scan-filter.ts +1 -1
  100. package/src/build/route-types/source-scan.ts +118 -0
  101. package/src/build/runtime-discovery.ts +9 -20
  102. package/src/cache/cache-scope.ts +28 -42
  103. package/src/cache/cf/cf-cache-store.ts +54 -13
  104. package/src/client.rsc.tsx +3 -0
  105. package/src/client.tsx +96 -205
  106. package/src/context-var.ts +5 -5
  107. package/src/decode-loader-results.ts +36 -0
  108. package/src/errors.ts +30 -4
  109. package/src/handle.ts +32 -14
  110. package/src/host/index.ts +2 -2
  111. package/src/host/router.ts +129 -57
  112. package/src/host/types.ts +31 -2
  113. package/src/host/utils.ts +1 -1
  114. package/src/href-client.ts +140 -21
  115. package/src/index.rsc.ts +10 -6
  116. package/src/index.ts +54 -17
  117. package/src/loader-store.ts +500 -0
  118. package/src/loader.rsc.ts +25 -7
  119. package/src/loader.ts +16 -9
  120. package/src/missing-id-error.ts +68 -0
  121. package/src/outlet-context.ts +1 -1
  122. package/src/prerender.ts +27 -6
  123. package/src/response-utils.ts +37 -0
  124. package/src/reverse.ts +65 -36
  125. package/src/route-content-wrapper.tsx +6 -28
  126. package/src/route-definition/dsl-helpers.ts +384 -257
  127. package/src/route-definition/helper-factories.ts +29 -139
  128. package/src/route-definition/helpers-types.ts +100 -28
  129. package/src/route-definition/resolve-handler-use.ts +6 -0
  130. package/src/route-definition/use-item-types.ts +32 -0
  131. package/src/route-types.ts +26 -41
  132. package/src/router/basename.ts +14 -0
  133. package/src/router/content-negotiation.ts +15 -2
  134. package/src/router/error-handling.ts +1 -1
  135. package/src/router/find-match.ts +54 -6
  136. package/src/router/handler-context.ts +21 -38
  137. package/src/router/intercept-resolution.ts +4 -18
  138. package/src/router/lazy-includes.ts +41 -22
  139. package/src/router/loader-resolution.ts +82 -36
  140. package/src/router/manifest.ts +41 -19
  141. package/src/router/match-api.ts +4 -3
  142. package/src/router/match-handlers.ts +63 -20
  143. package/src/router/match-middleware/cache-lookup.ts +44 -91
  144. package/src/router/match-middleware/cache-store.ts +3 -2
  145. package/src/router/match-result.ts +53 -32
  146. package/src/router/metrics.ts +1 -1
  147. package/src/router/middleware-types.ts +15 -26
  148. package/src/router/middleware.ts +99 -84
  149. package/src/router/pattern-matching.ts +116 -19
  150. package/src/router/prerender-match.ts +1 -1
  151. package/src/router/preview-match.ts +3 -1
  152. package/src/router/request-classification.ts +4 -28
  153. package/src/router/revalidation.ts +58 -2
  154. package/src/router/router-interfaces.ts +45 -28
  155. package/src/router/router-options.ts +40 -1
  156. package/src/router/router-registry.ts +2 -5
  157. package/src/router/segment-resolution/fresh.ts +27 -6
  158. package/src/router/segment-resolution/revalidation.ts +147 -106
  159. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  160. package/src/router/substitute-pattern-params.ts +56 -0
  161. package/src/router/telemetry.ts +99 -0
  162. package/src/router/trie-matching.ts +40 -16
  163. package/src/router/types.ts +8 -0
  164. package/src/router/url-params.ts +49 -0
  165. package/src/router.ts +52 -30
  166. package/src/rsc/handler-context.ts +2 -2
  167. package/src/rsc/handler.ts +28 -69
  168. package/src/rsc/helpers.ts +91 -43
  169. package/src/rsc/index.ts +1 -1
  170. package/src/rsc/manifest-init.ts +28 -41
  171. package/src/rsc/origin-guard.ts +28 -10
  172. package/src/rsc/progressive-enhancement.ts +4 -0
  173. package/src/rsc/response-error.ts +79 -12
  174. package/src/rsc/response-route-handler.ts +57 -61
  175. package/src/rsc/rsc-rendering.ts +35 -51
  176. package/src/rsc/runtime-warnings.ts +9 -10
  177. package/src/rsc/server-action.ts +17 -37
  178. package/src/rsc/ssr-setup.ts +16 -0
  179. package/src/rsc/types.ts +8 -2
  180. package/src/runtime-env.ts +18 -0
  181. package/src/search-params.ts +4 -4
  182. package/src/segment-content-promise.ts +67 -0
  183. package/src/segment-loader-promise.ts +122 -0
  184. package/src/segment-system.tsx +132 -116
  185. package/src/serialize.ts +243 -0
  186. package/src/server/context.ts +175 -53
  187. package/src/server/cookie-store.ts +28 -4
  188. package/src/server/request-context.ts +67 -51
  189. package/src/ssr/index.tsx +5 -1
  190. package/src/static-handler.ts +25 -3
  191. package/src/testing/cache-status.ts +166 -0
  192. package/src/testing/collect-handle.ts +63 -0
  193. package/src/testing/dispatch.ts +581 -0
  194. package/src/testing/dom.entry.ts +22 -0
  195. package/src/testing/e2e/fixture.ts +188 -0
  196. package/src/testing/e2e/index.ts +149 -0
  197. package/src/testing/e2e/matchers.ts +51 -0
  198. package/src/testing/e2e/page-helpers.ts +272 -0
  199. package/src/testing/e2e/parity.ts +326 -0
  200. package/src/testing/e2e/server.ts +195 -0
  201. package/src/testing/flight-matchers.ts +110 -0
  202. package/src/testing/flight-normalize.ts +38 -0
  203. package/src/testing/flight-runtime.d.ts +57 -0
  204. package/src/testing/flight-tree.ts +682 -0
  205. package/src/testing/flight.entry.ts +51 -0
  206. package/src/testing/flight.ts +234 -0
  207. package/src/testing/generated-routes.ts +223 -0
  208. package/src/testing/index.ts +106 -0
  209. package/src/testing/internal/context.ts +304 -0
  210. package/src/testing/internal/flight-client-globals.ts +30 -0
  211. package/src/testing/internal/seed-vars.ts +42 -0
  212. package/src/testing/render-handler.ts +323 -0
  213. package/src/testing/render-route.tsx +590 -0
  214. package/src/testing/run-loader.ts +363 -0
  215. package/src/testing/run-middleware.ts +205 -0
  216. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  217. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  218. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  219. package/src/testing/vitest-stubs/version.ts +5 -0
  220. package/src/testing/vitest.ts +285 -0
  221. package/src/types/global-namespace.ts +39 -26
  222. package/src/types/handler-context.ts +68 -50
  223. package/src/types/index.ts +1 -0
  224. package/src/types/loader-types.ts +11 -9
  225. package/src/types/request-scope.ts +126 -0
  226. package/src/types/route-entry.ts +11 -0
  227. package/src/types/segments.ts +35 -2
  228. package/src/urls/include-helper.ts +34 -67
  229. package/src/urls/index.ts +1 -5
  230. package/src/urls/path-helper-types.ts +41 -7
  231. package/src/urls/path-helper.ts +17 -52
  232. package/src/urls/pattern-types.ts +36 -19
  233. package/src/urls/response-types.ts +22 -29
  234. package/src/urls/type-extraction.ts +58 -139
  235. package/src/urls/urls-function.ts +1 -5
  236. package/src/use-loader.tsx +413 -42
  237. package/src/vite/debug.ts +185 -0
  238. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  239. package/src/vite/discovery/discover-routers.ts +106 -75
  240. package/src/vite/discovery/discovery-errors.ts +194 -0
  241. package/src/vite/discovery/gate-state.ts +171 -0
  242. package/src/vite/discovery/prerender-collection.ts +67 -26
  243. package/src/vite/discovery/route-types-writer.ts +40 -84
  244. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  245. package/src/vite/discovery/state.ts +33 -0
  246. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  247. package/src/vite/index.ts +2 -0
  248. package/src/vite/plugin-types.ts +67 -0
  249. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  250. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  251. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  252. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  253. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  254. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  255. package/src/vite/plugins/expose-action-id.ts +54 -30
  256. package/src/vite/plugins/expose-id-utils.ts +12 -8
  257. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  258. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  259. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  260. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  261. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  262. package/src/vite/plugins/performance-tracks.ts +29 -25
  263. package/src/vite/plugins/use-cache-transform.ts +65 -50
  264. package/src/vite/plugins/version-injector.ts +39 -23
  265. package/src/vite/plugins/version-plugin.ts +59 -2
  266. package/src/vite/plugins/virtual-entries.ts +2 -2
  267. package/src/vite/rango.ts +116 -29
  268. package/src/vite/router-discovery.ts +750 -100
  269. package/src/vite/utils/ast-handler-extract.ts +15 -15
  270. package/src/vite/utils/banner.ts +1 -1
  271. package/src/vite/utils/bundle-analysis.ts +4 -2
  272. package/src/vite/utils/client-chunks.ts +190 -0
  273. package/src/vite/utils/forward-user-plugins.ts +193 -0
  274. package/src/vite/utils/manifest-utils.ts +8 -59
  275. package/src/vite/utils/package-resolution.ts +41 -1
  276. package/src/vite/utils/prerender-utils.ts +21 -6
  277. package/src/vite/utils/shared-utils.ts +107 -26
  278. package/src/browser/action-response-classifier.ts +0 -99
@@ -1,16 +1,25 @@
1
1
  ---
2
2
  name: links
3
- description: URL generation with ctx.reverse (server), href (client), useHref (mounted), useMount, and scopedReverse
4
- argument-hint: [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
8
8
 
9
9
  @rangojs/router provides different href APIs for server and client contexts.
10
10
 
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
+
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.
19
+
11
20
  ## Server: ctx.reverse()
12
21
 
13
- Available in route handlers via HandlerContext. Resolves named routes using the full route map.
22
+ Available in route handlers via HandlerContext. Resolves named routes using the full route map. This is the default way to generate URLs on the server.
14
23
 
15
24
  ```typescript
16
25
  import { urls, scopedReverse } from "@rangojs/router";
@@ -103,7 +112,7 @@ path("/search", (ctx) => {
103
112
 
104
113
  ### scopedReverse() - type-safe ctx.reverse
105
114
 
106
- Wraps `ctx.reverse` with local route type information for autocomplete and validation:
115
+ Wraps `ctx.reverse` with local route type information for autocomplete and validation. Runtime behavior is identical to `ctx.reverse` — `scopedReverse` is a type-only cast. The same dot-prefix rule applies: local names use `.name`, global names use `name.sub`.
107
116
 
108
117
  ```typescript
109
118
  import { scopedReverse } from "@rangojs/router";
@@ -111,18 +120,83 @@ import { scopedReverse } from "@rangojs/router";
111
120
  path("/product/:slug", (ctx) => {
112
121
  const reverse = scopedReverse<typeof shopPatterns>(ctx.reverse);
113
122
 
114
- reverse("cart"); // Type-safe local name
115
- reverse("product", { slug: "widget" }); // Type-safe with params
116
- reverse("blog.post"); // Absolute names (dot notation) always allowed
117
- reverse("/about"); // Path-based always allowed
123
+ reverse(".cart"); // Local name (dot-prefixed) resolves in include scope
124
+ reverse(".product", { slug: "widget" }); // Local name with params
125
+ reverse("blog.post", { slug: "hi" }); // Global name (dotted) full route map
118
126
 
119
127
  return <ProductPage slug={ctx.params.slug} />;
120
128
  }, { name: "product" })
121
129
  ```
122
130
 
131
+ `reverse()` does not accept raw path strings (`"/about"`). For static paths in client components, use `href("/about")`; on the server, look up the route by name.
132
+
133
+ ## Client components: receive URLs as props
134
+
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:
136
+
137
+ 1. Pass as a prop from a server component:
138
+
139
+ ```tsx
140
+ // server
141
+ function BlogPostPage(ctx: HandlerContext) {
142
+ return <ShareButton url={ctx.reverse(".post", { slug: ctx.params.slug })} />;
143
+ }
144
+ ```
145
+
146
+ ```tsx
147
+ "use client";
148
+
149
+ export function ShareButton({ url }: { url: string }) {
150
+ return (
151
+ <button onClick={() => navigator.clipboard.writeText(url)}>Share</button>
152
+ );
153
+ }
154
+ ```
155
+
156
+ 2. Return from a loader (attached to the route via the DSL):
157
+
158
+ ```tsx
159
+ // server — loaders/nav.ts
160
+ export const NavLoader = createLoader((ctx) => ({
161
+ home: ctx.reverse("home"),
162
+ blog: ctx.reverse("blog.index"),
163
+ }));
164
+
165
+ // server — urls.tsx: attach the loader so useLoader has data in context
166
+ const urlpatterns = urls(({ path, loader }) => [
167
+ path("/", HomePage, { name: "home" }, () => [loader(NavLoader)]),
168
+ ]);
169
+ ```
170
+
171
+ ```tsx
172
+ "use client";
173
+
174
+ function Nav() {
175
+ const { data } = useLoader(NavLoader);
176
+ return <Link to={data.home}>Home</Link>;
177
+ }
178
+ ```
179
+
180
+ `useLoader()` requires the loader to be attached to an active route. If you need on-demand fetching instead, use `useFetchLoader()`.
181
+
182
+ 3. Return from a server action:
183
+
184
+ ```tsx
185
+ "use server";
186
+
187
+ export async function getProductUrl(slug: string) {
188
+ const ctx = getRequestContext();
189
+ return ctx.reverse("product", { slug });
190
+ }
191
+ ```
192
+
193
+ See `/server-actions` for the full action surface (`getRequestContext()` is the same context middleware and handlers use).
194
+
195
+ For static path strings (not named routes), client components can use `href()` — see below.
196
+
123
197
  ## Client: href()
124
198
 
125
- Plain function for absolute path-based URLs. No hook needed - works anywhere.
199
+ Plain function for absolute path-based URLs. No hook needed - works anywhere in client components. `href()` validates paths at compile time, but does **not** resolve named routes — for named routes, use one of the patterns above.
126
200
 
127
201
  ```typescript
128
202
  "use client";
@@ -139,7 +213,17 @@ function GlobalNav() {
139
213
  }
140
214
  ```
141
215
 
142
- `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
+ ```
143
227
 
144
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.
145
229
 
@@ -185,15 +269,161 @@ function MountInfo() {
185
269
 
186
270
  `useMount()` reads from `MountContext`, which is automatically set by `include()` in the segment tree.
187
271
 
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
+ ```
331
+
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
+
188
414
  ## When to use what
189
415
 
190
- | Context | API | Resolves | Use for |
191
- | ---------------- | ------------------------------- | ------------------------------- | ----------------------------------- |
192
- | Server handler | `ctx.reverse("name")` | Named routes (local + absolute) | Server-side URL generation |
193
- | Server handler | `scopedReverse<T>(ctx.reverse)` | Same, with type safety | Type-safe server URLs |
194
- | Client component | `href("/path")` | Absolute paths | Global navigation |
195
- | Client component | `useHref()` | Mount-prefixed paths | Local navigation inside `include()` |
196
- | Client component | `useMount()` | Raw mount path | Custom mount-aware logic |
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.
197
427
 
198
428
  ## Complete example: mounted module
199
429
 
@@ -6,7 +6,10 @@ argument-hint: [loader]
6
6
 
7
7
  # Data Loaders with loader()
8
8
 
9
- Loaders fetch data on the server and stream it to the client.
9
+ Loaders fetch data on the server and stream it to the client. For mutations
10
+ (writes triggered by forms or buttons), use server actions instead — see
11
+ `/server-actions`. Loaders re-resolve after an action runs, so the typical
12
+ flow is _action mutates → loader re-reads → UI updates_.
10
13
 
11
14
  ## Creating a Loader
12
15
 
@@ -88,6 +91,20 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
88
91
  ]);
89
92
  ```
90
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
+
91
108
  DSL loaders are the **live data layer** — they resolve fresh on every
92
109
  request, even when the route is inside a `cache()` boundary. The router
93
110
  excludes them from the segment cache at storage time and re-resolves them
@@ -139,7 +156,29 @@ same memoized result — loaders never run twice per request.
139
156
 
140
157
  ## Loader Context
141
158
 
142
- Loaders receive the same context as route handlers:
159
+ Loaders receive the same context shape as route handlers.
160
+
161
+ ### Full field surface
162
+
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). |
180
+
181
+ ### Example
143
182
 
144
183
  ```typescript
145
184
  export const ProductLoader = createLoader(async (ctx) => {
@@ -160,13 +199,24 @@ export const ProductLoader = createLoader(async (ctx) => {
160
199
  // Request headers
161
200
  const auth = ctx.request.headers.get("Authorization");
162
201
 
163
- // Variables set by middleware (from RSCRouter.Vars augmentation)
202
+ // Variables set by middleware (from Rango.Vars augmentation)
164
203
  const user = ctx.get("user");
165
204
 
166
- return { product: await fetchProduct(slug) };
205
+ // Type-checked URLs for payloads. `.name` resolves within the current
206
+ // include() scope; a bare `name` resolves globally. See /route and
207
+ // /typesafety for scope rules and route-name autocomplete.
208
+ const detailUrl = ctx.reverse(".detail", { slug });
209
+
210
+ return {
211
+ product: await fetchProduct(slug),
212
+ links: { self: detailUrl },
213
+ };
167
214
  });
168
215
  ```
169
216
 
217
+ See `/route` for the full handler-context contract (shared with loaders) and
218
+ `/typesafety` for route-name typing that powers `ctx.reverse` autocomplete.
219
+
170
220
  ### params vs routeParams
171
221
 
172
222
  - `ctx.params` — merged route params + explicit loader params. For fetchable
@@ -208,13 +258,104 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
208
258
  revalidate(() => false), // Never revalidate
209
259
  ]),
210
260
 
211
- // Loader that revalidates after cart actions
261
+ // Loader that revalidates after cart actions (defer otherwise — keeps the
262
+ // permissive loader defaults for navigation and other actions intact)
212
263
  loader(CartLoader, () => [
213
- revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
264
+ revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
214
265
  ]),
215
266
  ]);
216
267
  ```
217
268
 
269
+ ### `revalidate()` return shapes
270
+
271
+ > **Scope: `revalidate()` is a partial-render concern, not a cache concern.**
272
+ > It decides whether a segment (here, a loader) re-runs and streams to the
273
+ > client on a navigation or action — never whether a cached value is stale. The
274
+ > cache decides hit/miss/ttl/swr independently and never reads `revalidate()`.
275
+ > Caching a loader is a separate, opt-in step (`loader(Fn, () => [cache({...})])`).
276
+ > See `/cache-guide` → "Two axes" and `/rango` → "The shape of rango".
277
+
278
+ A `revalidate(fn)` callback can return one of four shapes. The chain
279
+ processes revalidators in order; each call's return controls how the
280
+ chain continues:
281
+
282
+ ```typescript
283
+ // 1) Hard decision — short-circuits the chain, used as the final answer.
284
+ revalidate(() => true);
285
+ revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
286
+
287
+ // 2) Soft decision — updates the running suggestion for downstream
288
+ // revalidators on the same segment, chain continues.
289
+ revalidate(({ defaultShouldRevalidate }) => ({
290
+ defaultShouldRevalidate: !defaultShouldRevalidate,
291
+ }));
292
+
293
+ // 3) Defer (no opinion) — leaves the running suggestion unchanged and
294
+ // continues to the next revalidator. Implicit return / null /
295
+ // undefined are all equivalent and consumer-friendly.
296
+ revalidate(({ actionId }) => {
297
+ if (actionId?.includes("Cart")) return true; // hard for this branch only
298
+ // implicit return — let downstream revalidators or the segment default decide
299
+ });
300
+ revalidate(() => undefined); // explicit defer
301
+ revalidate(() => null); // explicit defer
302
+ ```
303
+
304
+ If every revalidator on a segment defers, the segment-type default
305
+ (e.g. params-changed for routes, `false` for parallels) is used.
306
+
307
+ #### `|| undefined` (defer) vs `?? false` (hard) — pick deliberately
308
+
309
+ A boolean return — including `false` — is a **hard** decision: it short-circuits
310
+ the chain and overrides the segment default. `undefined` **defers** to the
311
+ running suggestion / segment default. They are not interchangeable:
312
+
313
+ ```typescript
314
+ // Defer: "revalidate on match, otherwise let the default/downstream decide."
315
+ revalidate(({ actionId }) => actionId?.includes("Cart") || undefined);
316
+
317
+ // Hard: "revalidate ONLY on match, suppress everything else."
318
+ revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
319
+ ```
320
+
321
+ This matters most for loaders, whose defaults are permissive: a loader defaults
322
+ to revalidating on **any** action (`POST`) and on **param/search changes**
323
+ during navigation. So `?? false` on a loader silently suppresses both — the
324
+ loader will not refetch when you navigate to a different `:id`. Use
325
+ `|| undefined` when you want to _add_ a revalidation signal on top of the
326
+ sensible defaults, and reserve `?? false` for the rare case where you genuinely
327
+ want the loader to refetch on nothing but your matched action.
328
+
329
+ When **composing multiple revalidators** on one segment (see below), defer is
330
+ mandatory: the first hard `?? false` ends the chain and the later contracts
331
+ never run.
332
+
333
+ #### Matching actions: `ctx.isAction()`
334
+
335
+ To revalidate after specific server actions, match them by **reference** with
336
+ `ctx.isAction()` rather than hand-written `actionId` substrings. A rename or
337
+ moved file then becomes a type error instead of silently failing to match:
338
+
339
+ ```typescript
340
+ import { addToCart, removeFromCart } from "../actions/cart";
341
+ import * as CartActions from "../actions/cart";
342
+
343
+ loader(CartLoader, () => [
344
+ revalidate((ctx) => ctx.isAction(addToCart) || undefined), // one action
345
+ ]);
346
+ revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
347
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any action in the module
348
+ ```
349
+
350
+ `isAction()` is a method on the revalidate predicate's **context argument** —
351
+ there is no standalone `isAction` import; you always reach it through the callback
352
+ parameter (`revalidate((ctx) => ctx.isAction(...))`). It returns a raw boolean, so
353
+ pair it with `|| undefined` for the usual "revalidate on match, else defer"
354
+ intent. It returns `false` on plain navigation and on non-matches, and resolves
355
+ the reference the same way the router derives `actionId` (`$id` in production,
356
+ `$$id` in dev), so it matches in both modes. The raw `actionId` string stays
357
+ available on the same context as an escape hatch.
358
+
218
359
  ### Revalidation Contracts for Loader Dependencies
219
360
 
220
361
  If a loader reads `ctx.get()` data produced by an outer handler/layout, share
@@ -222,8 +363,12 @@ the same named revalidation contract across producer and consumer segments.
222
363
 
223
364
  ```typescript
224
365
  // revalidation-contracts.ts
225
- export const revalidateAccountScope = ({ actionId }) =>
226
- actionId?.includes("src/actions/account.ts#") ?? false;
366
+ import * as AccountActions from "./actions/account";
367
+
368
+ // Match by reference with ctx.isAction() (rename-safe), and defer (|| undefined)
369
+ // so these contracts compose — a hard `false` would short-circuit the rest.
370
+ export const revalidateAccountScope = (ctx) =>
371
+ ctx.isAction(AccountActions) || undefined;
227
372
 
228
373
  layout(AccountLayout, () => [
229
374
  revalidate(revalidateAccountScope), // producer reruns
@@ -266,6 +411,64 @@ follows the same rule: at build time, loaders are skipped entirely (there is no
266
411
  real request context), and at runtime the worker resolves them fresh against
267
412
  the live database.
268
413
 
414
+ ### Parallel and streaming — latency overlaps first paint
415
+
416
+ Loaders do not block the page. As the render pass begins — the pass that route
417
+ middleware wraps, so loaders run right after middleware, not in a later
418
+ phase — every matched loader is kicked off **concurrently** (their promises start in the
419
+ same tick), and each result is **streamed** to the client as its own RSC Flight
420
+ chunk rather than awaited up front. Pair a loader with `loading()` (or a
421
+ client `<Suspense>`) and the shell paints immediately while the data streams in.
422
+
423
+ This is why **"cached UI still pays full data latency" is the wrong intuition**:
424
+ on a `cache()` hit the UI segments stream instantly from cache while the live
425
+ loaders resolve fresh **in parallel** — data latency _overlaps_ first paint
426
+ instead of being added on top of it. (Without a `loading()` / `<Suspense>`
427
+ boundary a parallel loader blocks its parent, so add one to keep the overlap.)
428
+
429
+ If you come from a framework where the loader is a blocking step that runs
430
+ before the response is built, this is the shift to internalize: here the
431
+ response starts streaming first and loader data fills in.
432
+
433
+ ### See it: `debugPerformance`
434
+
435
+ Turn on the per-request performance timeline early — it is the fastest way to
436
+ confirm loaders overlap rather than serialize, and to find the real bottleneck
437
+ locally instead of guessing:
438
+
439
+ ```typescript
440
+ const router = createRouter({ document: Document, debugPerformance: true });
441
+ ```
442
+
443
+ Or enable it per-request from middleware (e.g. only when `?debug` is present) by
444
+ calling `ctx.debugPerformance()` **before** `await next()`. Each HTML request
445
+ then prints a shared-axis waterfall (and emits a `Server-Timing` header):
446
+
447
+ ```
448
+ [RSC Perf] GET /product/widget (24.53ms)
449
+ start dur span timeline
450
+ 0.08ms 3.20ms route-matching |#####...................................|
451
+ 3.40ms 8.70ms ssr-render-html |.....##############.....................|
452
+ 3.42ms 11.90ms loader:…#ProductLoader |.....###################................|
453
+ 3.45ms 11.40ms loader:…#ReviewsLoader |.....##################.................|
454
+ 0.00ms 24.53ms handler:total |########################################|
455
+ ```
456
+
457
+ How to read it:
458
+
459
+ - **Humans:** scan the `#` bars on the shared axis. Bars that start at the same
460
+ offset and run side by side are executing **in parallel** — loaders should
461
+ overlap `ssr-render-html` / `render:total`, not sit alone to the right of
462
+ everything. A lone `loader:*` bar past the render bar is serialized latency to
463
+ chase. `handler:total` is the whole request; `render:total` is the render pass.
464
+ - **LLMs / programmatic:** read each row as `{ start, dur, label }`. A loader
465
+ overlaps paint when its `[start, start+dur]` interval intersects
466
+ `render:total` / `ssr-render-html`. Flag a regression when a `loader:*`
467
+ interval is **disjoint from and starts after** `render:total`, or when its
468
+ `dur` approaches `handler:total` — that loader is on the critical path instead
469
+ of overlapping it. Two `loader:*` rows with near-equal `start` confirm
470
+ parallel execution.
471
+
269
472
  ### Opting a Loader into Caching
270
473
 
271
474
  To cache a specific loader's data, attach a `cache()` child:
@@ -539,6 +742,13 @@ export const FileUploadLoader = createLoader(async (ctx) => {
539
742
 
540
743
  Client usage — see `/hooks useFetchLoader` for the full client-side pattern.
541
744
 
745
+ > **Refetch sharing**: when the loader is registered on the route via
746
+ > `loader()`, a plain `load()` call (no `params`, no `body`) broadcasts
747
+ > the new value to every component reading the same loader id —
748
+ > `useLoader` reads in layouts, pages, and parallel slots all converge.
749
+ > Calls with `params` or a non-GET method stay local to the call site.
750
+ > See `/hooks` → "Shared refetch behavior" for the full contract.
751
+
542
752
  ## Complete Example
543
753
 
544
754
  ```typescript
@@ -574,7 +784,7 @@ export const CartLoader = createLoader(async (ctx) => {
574
784
  export const urlpatterns = urls(({ path, layout, loader, loading, cache, revalidate }) => [
575
785
  layout(<ShopLayout />, () => [
576
786
  loader(CartLoader, () => [
577
- revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
787
+ revalidate(({ actionId }) => actionId?.includes("Cart") || undefined),
578
788
  ]),
579
789
 
580
790
  path("/shop/product/:slug", ProductPage, { name: "product" }, () => [