@rangojs/router 0.0.0-experimental.98 → 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 (355) 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 +60 -11
  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/intercept/SKILL.md +29 -5
  18. package/skills/layout/SKILL.md +13 -9
  19. package/skills/links/SKILL.md +173 -17
  20. package/skills/loader/SKILL.md +170 -23
  21. package/skills/middleware/SKILL.md +16 -10
  22. package/skills/migrate-nextjs/SKILL.md +38 -16
  23. package/skills/mime-routes/SKILL.md +27 -0
  24. package/skills/observability/SKILL.md +137 -0
  25. package/skills/parallel/SKILL.md +11 -7
  26. package/skills/prerender/SKILL.md +14 -33
  27. package/skills/rango/SKILL.md +250 -26
  28. package/skills/react-compiler/SKILL.md +168 -0
  29. package/skills/response-routes/SKILL.md +114 -47
  30. package/skills/route/SKILL.md +22 -5
  31. package/skills/router-setup/SKILL.md +3 -3
  32. package/skills/server-actions/SKILL.md +78 -42
  33. package/skills/tailwind/SKILL.md +27 -3
  34. package/skills/testing/SKILL.md +129 -0
  35. package/skills/testing/bindings.md +89 -0
  36. package/skills/testing/cache-prerender.md +124 -0
  37. package/skills/testing/client-components.md +122 -0
  38. package/skills/testing/e2e-parity.md +125 -0
  39. package/skills/testing/flight.md +92 -0
  40. package/skills/testing/handles.md +129 -0
  41. package/skills/testing/loader.md +128 -0
  42. package/skills/testing/middleware.md +99 -0
  43. package/skills/testing/render-handler.md +121 -0
  44. package/skills/testing/response-routes.md +95 -0
  45. package/skills/testing/reverse-and-types.md +84 -0
  46. package/skills/testing/server-actions.md +107 -0
  47. package/skills/testing/server-tree.md +128 -0
  48. package/skills/testing/setup.md +120 -0
  49. package/skills/typesafety/SKILL.md +310 -26
  50. package/skills/use-cache/SKILL.md +36 -5
  51. package/skills/vercel/SKILL.md +107 -0
  52. package/skills/view-transitions/SKILL.md +294 -0
  53. package/src/__augment-tests__/augment.ts +81 -0
  54. package/src/__augment-tests__/augmented.check.ts +116 -0
  55. package/src/__internal.ts +0 -65
  56. package/src/browser/action-coordinator.ts +53 -36
  57. package/src/browser/action-fence.ts +47 -0
  58. package/src/browser/app-shell.ts +14 -27
  59. package/src/browser/cookie-name.ts +140 -0
  60. package/src/browser/event-controller.ts +37 -143
  61. package/src/browser/history-state.ts +21 -0
  62. package/src/browser/index.ts +3 -3
  63. package/src/browser/invalidate-client-cache.ts +52 -0
  64. package/src/browser/navigation-bridge.ts +30 -59
  65. package/src/browser/navigation-client.ts +96 -84
  66. package/src/browser/navigation-store-handle.ts +38 -0
  67. package/src/browser/navigation-store.ts +32 -82
  68. package/src/browser/navigation-transaction.ts +9 -59
  69. package/src/browser/partial-update.ts +60 -127
  70. package/src/browser/prefetch/cache.ts +82 -72
  71. package/src/browser/prefetch/fetch.ts +108 -33
  72. package/src/browser/prefetch/queue.ts +6 -3
  73. package/src/browser/rango-state.ts +157 -115
  74. package/src/browser/react/Link.tsx +0 -2
  75. package/src/browser/react/NavigationProvider.tsx +41 -48
  76. package/src/browser/react/ScrollRestoration.tsx +10 -6
  77. package/src/browser/react/filter-segment-order.ts +0 -2
  78. package/src/browser/react/index.ts +0 -48
  79. package/src/browser/react/location-state-shared.ts +166 -8
  80. package/src/browser/react/location-state.ts +39 -14
  81. package/src/browser/react/use-action.ts +6 -15
  82. package/src/browser/react/use-handle.ts +17 -14
  83. package/src/browser/react/use-link-status.ts +0 -4
  84. package/src/browser/react/use-navigation.ts +0 -3
  85. package/src/browser/react/use-params.ts +3 -6
  86. package/src/browser/react/use-reverse.ts +106 -0
  87. package/src/browser/react/use-router.ts +20 -5
  88. package/src/browser/react/use-search-params.ts +0 -5
  89. package/src/browser/react/use-segments.ts +0 -13
  90. package/src/browser/response-adapter.ts +52 -1
  91. package/src/browser/rsc-router.tsx +70 -34
  92. package/src/browser/scroll-restoration.ts +22 -14
  93. package/src/browser/segment-structure-assert.ts +2 -2
  94. package/src/browser/server-action-bridge.ts +168 -44
  95. package/src/browser/types.ts +36 -21
  96. package/src/browser/validate-redirect-origin.ts +43 -16
  97. package/src/build/collect-fallback-refs.ts +107 -0
  98. package/src/build/generate-manifest.ts +60 -35
  99. package/src/build/generate-route-types.ts +3 -0
  100. package/src/build/index.ts +8 -2
  101. package/src/build/prefix-tree-utils.ts +123 -0
  102. package/src/build/route-trie.ts +89 -11
  103. package/src/build/route-types/codegen.ts +4 -4
  104. package/src/build/route-types/include-resolution.ts +1 -1
  105. package/src/build/route-types/param-extraction.ts +6 -3
  106. package/src/build/route-types/per-module-writer.ts +7 -4
  107. package/src/build/route-types/router-processing.ts +122 -22
  108. package/src/build/route-types/scan-filter.ts +1 -1
  109. package/src/build/route-types/source-scan.ts +118 -0
  110. package/src/build/runtime-discovery.ts +9 -20
  111. package/src/cache/cache-error.ts +104 -0
  112. package/src/cache/cache-policy.ts +68 -28
  113. package/src/cache/cache-runtime.ts +134 -32
  114. package/src/cache/cache-scope.ts +100 -74
  115. package/src/cache/cache-tag.ts +98 -0
  116. package/src/cache/cf/cf-cache-store.ts +2255 -238
  117. package/src/cache/cf/index.ts +6 -16
  118. package/src/cache/document-cache.ts +61 -20
  119. package/src/cache/handle-snapshot.ts +63 -0
  120. package/src/cache/index.ts +22 -20
  121. package/src/cache/memory-segment-store.ts +136 -37
  122. package/src/cache/profile-registry.ts +6 -30
  123. package/src/cache/read-through-swr.ts +41 -11
  124. package/src/cache/segment-codec.ts +0 -16
  125. package/src/cache/tag-invalidation.ts +230 -0
  126. package/src/cache/types.ts +33 -100
  127. package/src/cache/vercel/index.ts +11 -0
  128. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  129. package/src/client.rsc.tsx +6 -21
  130. package/src/client.tsx +25 -61
  131. package/src/component-utils.ts +19 -0
  132. package/src/context-var.ts +17 -5
  133. package/src/decode-loader-results.ts +36 -0
  134. package/src/defer.ts +196 -0
  135. package/src/deps/ssr.ts +0 -1
  136. package/src/errors.ts +30 -4
  137. package/src/handle.ts +31 -23
  138. package/src/handles/MetaTags.tsx +0 -14
  139. package/src/handles/breadcrumbs.ts +16 -5
  140. package/src/handles/meta.ts +0 -39
  141. package/src/host/cookie-handler.ts +0 -36
  142. package/src/host/errors.ts +0 -24
  143. package/src/host/index.ts +8 -2
  144. package/src/host/pattern-matcher.ts +7 -50
  145. package/src/host/router.ts +107 -99
  146. package/src/host/testing.ts +40 -27
  147. package/src/host/types.ts +37 -4
  148. package/src/host/utils.ts +1 -1
  149. package/src/href-client.ts +137 -22
  150. package/src/index.rsc.ts +63 -9
  151. package/src/index.ts +64 -9
  152. package/src/internal-debug.ts +2 -4
  153. package/src/loader-store.ts +500 -0
  154. package/src/loader.rsc.ts +20 -13
  155. package/src/loader.ts +12 -11
  156. package/src/missing-id-error.ts +68 -0
  157. package/src/network-error-thrower.tsx +1 -6
  158. package/src/outlet-provider.tsx +1 -5
  159. package/src/prerender/param-hash.ts +10 -11
  160. package/src/prerender/store.ts +32 -37
  161. package/src/prerender.ts +61 -6
  162. package/src/redirect-origin.ts +100 -0
  163. package/src/response-utils.ts +9 -0
  164. package/src/reverse.ts +65 -41
  165. package/src/root-error-boundary.tsx +1 -19
  166. package/src/route-content-wrapper.tsx +7 -72
  167. package/src/route-definition/dsl-helpers.ts +244 -281
  168. package/src/route-definition/helper-factories.ts +29 -139
  169. package/src/route-definition/helpers-types.ts +40 -17
  170. package/src/route-definition/redirect.ts +43 -9
  171. package/src/route-definition/resolve-handler-use.ts +6 -0
  172. package/src/route-definition/use-item-types.ts +32 -0
  173. package/src/route-map-builder.ts +0 -16
  174. package/src/route-types.ts +19 -41
  175. package/src/router/basename.ts +14 -0
  176. package/src/router/content-negotiation.ts +15 -15
  177. package/src/router/error-handling.ts +13 -17
  178. package/src/router/find-match.ts +44 -23
  179. package/src/router/handler-context.ts +4 -42
  180. package/src/router/intercept-resolution.ts +14 -19
  181. package/src/router/lazy-includes.ts +9 -46
  182. package/src/router/loader-resolution.ts +91 -46
  183. package/src/router/logging.ts +0 -6
  184. package/src/router/manifest.ts +18 -29
  185. package/src/router/match-api.ts +0 -20
  186. package/src/router/match-context.ts +0 -22
  187. package/src/router/match-handlers.ts +57 -58
  188. package/src/router/match-middleware/background-revalidation.ts +0 -7
  189. package/src/router/match-middleware/cache-lookup.ts +150 -271
  190. package/src/router/match-middleware/cache-store.ts +3 -33
  191. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  192. package/src/router/match-middleware/segment-resolution.ts +0 -22
  193. package/src/router/match-pipelines.ts +1 -42
  194. package/src/router/match-result.ts +31 -80
  195. package/src/router/metrics.ts +0 -34
  196. package/src/router/middleware-types.ts +0 -116
  197. package/src/router/middleware.ts +118 -133
  198. package/src/router/navigation-snapshot.ts +0 -51
  199. package/src/router/params-util.ts +23 -0
  200. package/src/router/pattern-matching.ts +20 -58
  201. package/src/router/prerender-match.ts +99 -63
  202. package/src/router/preview-match.ts +3 -1
  203. package/src/router/request-classification.ts +28 -62
  204. package/src/router/revalidation.ts +50 -56
  205. package/src/router/route-snapshot.ts +0 -1
  206. package/src/router/router-context.ts +0 -27
  207. package/src/router/router-interfaces.ts +68 -35
  208. package/src/router/router-options.ts +55 -1
  209. package/src/router/router-registry.ts +2 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -63
  211. package/src/router/segment-resolution/helpers.ts +34 -0
  212. package/src/router/segment-resolution/loader-cache.ts +40 -37
  213. package/src/router/segment-resolution/revalidation.ts +203 -285
  214. package/src/router/segment-resolution/static-store.ts +19 -5
  215. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  216. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  217. package/src/router/segment-resolution.ts +4 -1
  218. package/src/router/segment-wrappers.ts +0 -3
  219. package/src/router/state-cookie-name.ts +33 -0
  220. package/src/router/substitute-pattern-params.ts +56 -0
  221. package/src/router/telemetry-otel.ts +0 -20
  222. package/src/router/telemetry.ts +96 -19
  223. package/src/router/timeout.ts +0 -20
  224. package/src/router/trie-matching.ts +87 -47
  225. package/src/router/types.ts +9 -63
  226. package/src/router/url-params.ts +0 -5
  227. package/src/router.ts +80 -41
  228. package/src/rsc/handler-context.ts +3 -2
  229. package/src/rsc/handler.ts +83 -78
  230. package/src/rsc/helpers.ts +93 -5
  231. package/src/rsc/index.ts +1 -1
  232. package/src/rsc/json-route-result.ts +38 -0
  233. package/src/rsc/manifest-init.ts +28 -41
  234. package/src/rsc/origin-guard.ts +39 -25
  235. package/src/rsc/progressive-enhancement.ts +12 -1
  236. package/src/rsc/redirect-guard.ts +99 -0
  237. package/src/rsc/response-error.ts +79 -12
  238. package/src/rsc/response-route-handler.ts +76 -62
  239. package/src/rsc/rsc-rendering.ts +41 -60
  240. package/src/rsc/runtime-warnings.ts +23 -10
  241. package/src/rsc/server-action.ts +62 -67
  242. package/src/rsc/ssr-setup.ts +16 -0
  243. package/src/rsc/types.ts +10 -5
  244. package/src/runtime-env.ts +18 -0
  245. package/src/search-params.ts +4 -20
  246. package/src/segment-loader-promise.ts +14 -2
  247. package/src/segment-system.tsx +199 -142
  248. package/src/serialize.ts +243 -0
  249. package/src/server/context.ts +150 -51
  250. package/src/server/cookie-store.ts +80 -5
  251. package/src/server/handle-store.ts +7 -24
  252. package/src/server/loader-registry.ts +5 -24
  253. package/src/server/request-context.ts +165 -87
  254. package/src/ssr/index.tsx +14 -14
  255. package/src/static-handler.ts +10 -13
  256. package/src/testing/cache-status.ts +162 -0
  257. package/src/testing/collect-handle.ts +40 -0
  258. package/src/testing/dispatch.ts +618 -0
  259. package/src/testing/dom.entry.ts +22 -0
  260. package/src/testing/e2e/fixture.ts +188 -0
  261. package/src/testing/e2e/index.ts +128 -0
  262. package/src/testing/e2e/matchers.ts +35 -0
  263. package/src/testing/e2e/page-helpers.ts +272 -0
  264. package/src/testing/e2e/parity.ts +387 -0
  265. package/src/testing/e2e/server.ts +195 -0
  266. package/src/testing/flight-matchers.ts +97 -0
  267. package/src/testing/flight-normalize.ts +11 -0
  268. package/src/testing/flight-runtime.d.ts +57 -0
  269. package/src/testing/flight-tree.ts +682 -0
  270. package/src/testing/flight.entry.ts +52 -0
  271. package/src/testing/flight.ts +232 -0
  272. package/src/testing/generated-routes.ts +183 -0
  273. package/src/testing/index.ts +99 -0
  274. package/src/testing/internal/context.ts +348 -0
  275. package/src/testing/internal/flight-client-globals.ts +30 -0
  276. package/src/testing/internal/seed-vars.ts +54 -0
  277. package/src/testing/render-handler.ts +330 -0
  278. package/src/testing/render-route.tsx +566 -0
  279. package/src/testing/run-loader.ts +378 -0
  280. package/src/testing/run-middleware.ts +205 -0
  281. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  282. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  283. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  284. package/src/testing/vitest-stubs/version.ts +5 -0
  285. package/src/testing/vitest.ts +305 -0
  286. package/src/theme/ThemeProvider.tsx +0 -52
  287. package/src/theme/ThemeScript.tsx +0 -6
  288. package/src/theme/constants.ts +0 -12
  289. package/src/theme/index.ts +0 -7
  290. package/src/theme/theme-context.ts +1 -5
  291. package/src/theme/theme-script.ts +0 -14
  292. package/src/theme/use-theme.ts +0 -3
  293. package/src/types/boundaries.ts +0 -35
  294. package/src/types/cache-types.ts +13 -4
  295. package/src/types/error-types.ts +30 -90
  296. package/src/types/global-namespace.ts +54 -41
  297. package/src/types/handler-context.ts +97 -22
  298. package/src/types/index.ts +1 -10
  299. package/src/types/loader-types.ts +6 -3
  300. package/src/types/request-scope.ts +0 -19
  301. package/src/types/route-config.ts +6 -50
  302. package/src/types/route-entry.ts +0 -6
  303. package/src/types/segments.ts +18 -14
  304. package/src/urls/include-helper.ts +9 -56
  305. package/src/urls/index.ts +1 -11
  306. package/src/urls/path-helper-types.ts +19 -5
  307. package/src/urls/path-helper.ts +17 -106
  308. package/src/urls/pattern-types.ts +36 -19
  309. package/src/urls/response-types.ts +20 -19
  310. package/src/urls/type-extraction.ts +58 -139
  311. package/src/urls/urls-function.ts +1 -18
  312. package/src/use-loader.tsx +292 -107
  313. package/src/vite/debug.ts +1 -0
  314. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  315. package/src/vite/discovery/discover-routers.ts +95 -82
  316. package/src/vite/discovery/discovery-errors.ts +194 -0
  317. package/src/vite/discovery/prerender-collection.ts +26 -34
  318. package/src/vite/discovery/route-types-writer.ts +40 -84
  319. package/src/vite/discovery/state.ts +39 -1
  320. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  321. package/src/vite/index.ts +4 -0
  322. package/src/vite/plugin-types.ts +185 -10
  323. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  324. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  325. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  326. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  327. package/src/vite/plugins/expose-action-id.ts +4 -75
  328. package/src/vite/plugins/expose-id-utils.ts +3 -54
  329. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  330. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  331. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  332. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  333. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  334. package/src/vite/plugins/performance-tracks.ts +9 -16
  335. package/src/vite/plugins/refresh-cmd.ts +1 -1
  336. package/src/vite/plugins/use-cache-transform.ts +26 -49
  337. package/src/vite/plugins/vercel-output.ts +258 -0
  338. package/src/vite/plugins/version-injector.ts +2 -32
  339. package/src/vite/plugins/version-plugin.ts +32 -23
  340. package/src/vite/plugins/virtual-entries.ts +35 -17
  341. package/src/vite/rango.ts +148 -115
  342. package/src/vite/router-discovery.ts +220 -68
  343. package/src/vite/utils/ast-handler-extract.ts +15 -31
  344. package/src/vite/utils/bundle-analysis.ts +10 -15
  345. package/src/vite/utils/client-chunks.ts +184 -0
  346. package/src/vite/utils/forward-user-plugins.ts +171 -0
  347. package/src/vite/utils/manifest-utils.ts +4 -59
  348. package/src/vite/utils/package-resolution.ts +1 -73
  349. package/src/vite/utils/prerender-utils.ts +0 -35
  350. package/src/vite/utils/shared-utils.ts +95 -43
  351. package/src/browser/action-response-classifier.ts +0 -99
  352. package/src/browser/react/use-client-cache.ts +0 -58
  353. package/src/browser/shallow.ts +0 -40
  354. package/src/handles/index.ts +0 -7
  355. package/src/router/middleware-cookies.ts +0 -55
@@ -22,9 +22,9 @@ import { createHostRouter } from "@rangojs/router/host";
22
22
 
23
23
  const router = createHostRouter();
24
24
 
25
- router.host(["."]).map(() => import("./apps/main"));
26
- router.host(["admin.*"]).map(() => import("./apps/admin"));
27
- router.host(["api.*"]).map(() => import("./apps/api"));
25
+ router.host(["."]).lazy(() => import("./apps/main"));
26
+ router.host(["admin.*"]).lazy(() => import("./apps/admin"));
27
+ router.host(["api.*"]).lazy(() => import("./apps/api"));
28
28
 
29
29
  export default {
30
30
  fetch(request: Request, env: Env, ctx: ExecutionContext) {
@@ -33,7 +33,71 @@ export default {
33
33
  };
34
34
  ```
35
35
 
36
- Each `.map()` receives either a direct handler `(request, input) => Response` or a lazy import `() => import(...)`. Lazy imports resolve a module with a `default` export that is either a handler function or another `HostRouter` (for nesting).
36
+ ## Deploying: Cloudflare vs node/vercel
37
+
38
+ How a host router is _served_ depends on the preset, because the preset decides who owns the server entry.
39
+
40
+ | Preset | Who owns the entry | What the host module exports |
41
+ | ----------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------- |
42
+ | `cloudflare` | You (your `worker.rsc.tsx`) | `export default { fetch(request, env, ctx) { return router.match(request, { env, ctx }); } }` |
43
+ | `node` / `vercel` | rango (generated RSC entry) | `export default router;` (the `HostRouter` instance itself), or a named `export const hostRouter`/`router`. |
44
+
45
+ On `node`/`vercel`, rango generates the served RSC entry, so it needs the `HostRouter` **instance** to call `hostRouter.match()` for you. Export the instance, not a `{ fetch }` object:
46
+
47
+ ```typescript
48
+ // src/worker.rsc.tsx (node / vercel)
49
+ import { createHostRouter } from "@rangojs/router/host";
50
+
51
+ export const hostRouter = createHostRouter();
52
+ hostRouter.host(["admin.*"]).lazy(() => import("./apps/admin/handler.js"));
53
+ hostRouter.host(["."]).lazy(() => import("./apps/site/handler.js"));
54
+
55
+ // Export the instance — the generated entry serves it via hostRouter.match().
56
+ export default hostRouter;
57
+ ```
58
+
59
+ Each sub-app exports a handler exactly as on Cloudflare (no change):
60
+
61
+ ```typescript
62
+ // src/apps/admin/handler.ts
63
+ import { router } from "./router.js";
64
+ export default (request: Request, input: any) => router.fetch(request, input);
65
+ ```
66
+
67
+ Selecting the host entry — a host app has several `createRouter()` sub-apps, so single-router auto-discovery can't pick one. Either let rango auto-detect the lone `createHostRouter()` file, or point at it explicitly:
68
+
69
+ ```typescript
70
+ // vite.config.ts
71
+ rango({ preset: "vercel", host: "./src/worker.rsc.tsx" });
72
+ ```
73
+
74
+ On Vercel this is a single function running `hostRouter.match()` for every request (mirrors the Cloudflare single-worker model); `{ env, ctx }` (`process.env` + `{ waitUntil }`) is threaded unchanged to each matched sub-app's handler and `cache(env, ctx)` factory. See the `vercel` skill.
75
+
76
+ ## Inline handlers (`.map`) vs lazy mounts (`.lazy`)
77
+
78
+ A host pattern maps to one of two things, and you pick the method by intent:
79
+
80
+ | Method | Argument | Use for |
81
+ | ------- | ------------------------------ | ------------------------------------------------------------ |
82
+ | `.map` | `(request, input) => Response` | An inline request handler that produces a response directly. |
83
+ | `.lazy` | `() => import("./sub-app")` | A lazily-imported handler or nested host router (a sub-app). |
84
+
85
+ ```typescript
86
+ // Lazy mount: the module's default export is a handler or a HostRouter.
87
+ router.host(["admin.*"]).lazy(() => import("./apps/admin"));
88
+
89
+ // Inline handler: returns a Response itself (sync or async).
90
+ router.host(["health.*"]).map(() => new Response("ok"));
91
+ router
92
+ .host(["echo.*"])
93
+ .map((request) => new Response(new URL(request.url).pathname));
94
+ ```
95
+
96
+ Why two methods instead of one overloaded `.map()`:
97
+
98
+ - **Build-time discovery** invokes only `.lazy()` mounts (to trigger each sub-app's `createRouter()` registration). Inline `.map()` handlers are never invoked during discovery, so they can't crash it or pollute its errors.
99
+ - `.map(() => import("./sub-app"))` is a **type error** — a lazy import resolves to a module, not a `Response`. Use `.lazy()` for imports. (If the types are bypassed, e.g. from JS, a `.map()` handler that resolves to a module throws a clear `HostRouterError` at request time instead of returning the module.)
100
+ - A lazy loader may declare an ignored parameter (`.lazy((_request?) => import("./x"))`); `.lazy()` accepts it because intent is explicit, not inferred from the signature.
37
101
 
38
102
  ## Pattern Syntax
39
103
 
@@ -65,8 +129,8 @@ const hosts = defineHosts({
65
129
  app: [".", "www.*"],
66
130
  });
67
131
 
68
- router.host(hosts.admin).map(() => import("./apps/admin"));
69
- router.host(hosts.app).map(() => import("./apps/main"));
132
+ router.host(hosts.admin).lazy(() => import("./apps/admin"));
133
+ router.host(hosts.app).lazy(() => import("./apps/main"));
70
134
  ```
71
135
 
72
136
  Returns a frozen object — keys are autocompleted by TypeScript.
@@ -88,7 +152,7 @@ router.use(async (request, input, next) => {
88
152
  router
89
153
  .host(["admin.*"])
90
154
  .use(requireAuth)
91
- .map(() => import("./apps/admin"));
155
+ .lazy(() => import("./apps/admin"));
92
156
  ```
93
157
 
94
158
  Middleware signature: `(request: Request, input: RouterRequestInput, next: () => Promise<Response>) => Promise<Response>`
@@ -165,12 +229,26 @@ Logs pattern matching, route registration, and cookie override decisions to cons
165
229
  ## Testing
166
230
 
167
231
  ```typescript
168
- import { createTestRequest, testPattern } from "@rangojs/router/host/testing";
232
+ import {
233
+ createTestRequest,
234
+ testPattern,
235
+ matchesHost,
236
+ } from "@rangojs/router/host/testing";
169
237
 
170
- // Test pattern matching
238
+ // Test pattern matching (host-only)
171
239
  testPattern("admin.*", "admin.example.com"); // true
172
240
  testPattern([".", "www.*"], "example.com"); // true
173
241
 
242
+ // Path-based patterns need the third pathname arg (defaults to "/", so a
243
+ // host-only pattern still works with two args):
244
+ testPattern("**.workers.dev/admin", "foo.workers.dev", "/admin"); // true
245
+
246
+ // Or match a pattern against a real Request (hostname + pathname from the URL):
247
+ matchesHost(
248
+ "**.workers.dev/admin",
249
+ new Request("https://foo.workers.dev/admin"),
250
+ ); // true
251
+
174
252
  // Create requests for integration tests
175
253
  const request = createTestRequest({
176
254
  host: "admin.example.com",
@@ -179,40 +257,62 @@ const request = createTestRequest({
179
257
  });
180
258
 
181
259
  // Test which route would match (without executing)
182
- router.test("admin.example.com"); // { pattern, handler } | null
260
+ router.test("admin.example.com"); // { pattern, handler, kind } | null
183
261
  ```
184
262
 
185
263
  ## Error Types
186
264
 
187
265
  All errors extend `HostRouterError`:
188
266
 
189
- | Error | When |
190
- | ----------------------------- | ------------------------------------------- |
191
- | `InvalidPatternError` | Pattern is empty, non-string, or has spaces |
192
- | `HostOverrideNotAllowedError` | Cookie override from disallowed host |
193
- | `InvalidHostnameError` | Cookie value isn't a valid hostname |
194
- | `HostValidationError` | Custom `validate` function threw |
195
- | `NoRouteMatchError` | No host pattern matched the request |
196
- | `InvalidHandlerError` | Handler is not a function |
267
+ | Error | When |
268
+ | ----------------------------- | ------------------------------------------------------------------------------------------------- |
269
+ | `InvalidPatternError` | Pattern is empty, non-string, or has spaces |
270
+ | `HostOverrideNotAllowedError` | Cookie override from disallowed host |
271
+ | `InvalidHostnameError` | Cookie value isn't a valid hostname |
272
+ | `HostValidationError` | Custom `validate` function threw |
273
+ | `NoRouteMatchError` | No host pattern matched the request |
274
+ | `InvalidHandlerError` | Handler is not a function, or a lazy mount resolved to a module without a usable `default` export |
275
+ | `HostRouterError` | A `.map()` inline handler resolved to a module namespace (a misused lazy import — use `.lazy()`) |
197
276
 
198
277
  See the fallback section above for a `NoRouteMatchError` catch example.
199
278
 
200
279
  ## Nesting Host Routers
201
280
 
202
- A lazy handler can resolve to another `HostRouter`:
281
+ A lazy mount can resolve to another `HostRouter`:
203
282
 
204
283
  ```typescript
205
284
  // apps/regional.ts
206
285
  import { createHostRouter } from "@rangojs/router/host";
207
286
 
208
287
  const regional = createHostRouter();
209
- regional.host(["us.*"]).map(() => import("./regions/us"));
210
- regional.host(["eu.*"]).map(() => import("./regions/eu"));
288
+ regional.host(["us.*"]).lazy(() => import("./regions/us"));
289
+ regional.host(["eu.*"]).lazy(() => import("./regions/eu"));
211
290
 
212
291
  export default regional;
213
292
  ```
214
293
 
215
294
  ```typescript
216
295
  // host-router.ts
217
- router.host(["**.regional.example.com"]).map(() => import("./apps/regional"));
296
+ router.host(["**.regional.example.com"]).lazy(() => import("./apps/regional"));
218
297
  ```
298
+
299
+ ## Cross-app navigation is a full document load
300
+
301
+ A client-side navigation that crosses an app boundary (e.g. a `<Link>` or
302
+ intercepted `<a>` from the app at `/` into an app mounted at `/shop`) is a **hard
303
+ document navigation**, not a soft in-tree swap. When the server sees a partial
304
+ (SPA) request whose router id doesn't match the matched app, it returns
305
+ `X-RSC-Reload` and the client does a real document navigation to the target.
306
+
307
+ Why a reload rather than a soft swap: a soft swap can't faithfully re-establish
308
+ the target app's **document-level** state. Stylesheets shared across apps are
309
+ dropped by React 19's by-`href` resource dedup; and theme, warmup, and
310
+ prefetch-TTL are document-lifetime (captured once at load — see
311
+ `browser/app-shell.ts`), so the target app's config would never take effect. A
312
+ full document load re-establishes the target app's entire document — CSS, theme,
313
+ meta, everything — by construction. So you do **not** need to coordinate
314
+ stylesheet `href`s, `precedence`, theme config, etc. across independently-authored
315
+ apps; each app owns its own document.
316
+
317
+ **Within-app** navigation is unchanged — a normal soft SPA update (the document
318
+ stays mounted). Only crossing an app boundary triggers the reload.
@@ -8,9 +8,6 @@ argument-hint: [@slot-name] [route-to-intercept]
8
8
 
9
9
  Intercept routes render a different component during soft navigation (client-side) while preserving the background route. Hard navigation (direct URL) shows the full page.
10
10
 
11
- Canonical semantics reference:
12
- [docs/execution-model.md](../../docs/internal/execution-model.md)
13
-
14
11
  ## Basic Intercept
15
12
 
16
13
  ```typescript
@@ -110,8 +107,10 @@ Use named revalidation contracts on both the outer producer and the intercept
110
107
  consumer when they share `ctx.set()` data:
111
108
 
112
109
  ```typescript
113
- export const revalidateProductShell = ({ actionId }) =>
114
- actionId?.includes("src/actions/product.ts#") ?? false;
110
+ import * as ProductActions from "./actions/product";
111
+
112
+ export const revalidateProductShell = (ctx) =>
113
+ ctx.isAction(ProductActions) || undefined;
115
114
 
116
115
  layout(ProductLayout, () => [
117
116
  revalidate(revalidateProductShell), // producer reruns
@@ -197,6 +196,31 @@ function ModalWrapper({ children }) {
197
196
  }
198
197
  ```
199
198
 
199
+ ## Interaction with View Transitions
200
+
201
+ A layout that owns the `@modal` slot can also configure `transition()` for page
202
+ fades — opening a modal does **not** fire the layout's view transition. Rango
203
+ narrows the layout's `<ViewTransition>` wrap to the layout's default outlet
204
+ content, so `<ParallelOutlet />` (the slot where the modal mounts) is a sibling
205
+ of the wrap, not inside its subtree. Form actions submitted from inside an open
206
+ modal also commit without firing the underlying layout's transition, and the
207
+ modal subtree identity is preserved across revalidation (no remount,
208
+ `useActionState` survives). Closing the modal restores the page without a
209
+ stray transition.
210
+
211
+ For a modal-only morph (e.g. when intercepted URLs change while the modal
212
+ stays open), use an element-level React `<ViewTransition>` inside the modal
213
+ component — `transition()` accepted on `intercept()` via the DSL is not
214
+ applied to slot rendering today.
215
+
216
+ Caveat: route-level `transition()` wraps the route component itself, so a
217
+ `<ParallelOutlet />` rendered directly inside that route component would still
218
+ be inside the route's VT subtree. Mount the slot in a layout instead when you
219
+ combine intercept modals with route-level transitions.
220
+
221
+ See [skills/view-transitions](../view-transitions/SKILL.md) for the full
222
+ contract and direction-aware examples.
223
+
200
224
  ## Interaction with Prerender
201
225
 
202
226
  When the target route of an intercept uses `Prerender`, the intercept handler is
@@ -8,9 +8,6 @@ argument-hint: [component]
8
8
 
9
9
  Layouts wrap child routes and persist during navigation within their scope.
10
10
 
11
- Canonical semantics reference:
12
- [docs/execution-model.md](../../docs/internal/execution-model.md)
13
-
14
11
  ## Basic Layout
15
12
 
16
13
  ```typescript
@@ -118,6 +115,8 @@ function ShopLayout() {
118
115
  }
119
116
  ```
120
117
 
118
+ A layout's `transition()` config wraps the content that flows through `<Outlet />` — not the layout chrome itself, and not sibling `<ParallelOutlet />` slots. Stacking transitions across nested layouts collapses around the deepest default outlet content. See [skills/view-transitions](../view-transitions/SKILL.md) for the full wrap rules and intercept-modal interaction.
119
+
121
120
  ## Named Outlets
122
121
 
123
122
  For parallel routes, use named outlets:
@@ -203,8 +202,10 @@ layout(<ShopLayout />, () => [
203
202
  ])
204
203
 
205
204
  // Or revalidate based on conditions
205
+ import * as CartActions from "./actions/cart";
206
+
206
207
  layout(<CartLayout />, () => [
207
- revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
208
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
208
209
 
209
210
  path("/cart", CartPage, { name: "cart" }),
210
211
  ])
@@ -222,8 +223,9 @@ them on both producer and consumer segments:
222
223
 
223
224
  ```typescript
224
225
  // revalidation-contracts.ts
225
- export const revalidateCartData = ({ actionId }) =>
226
- actionId?.includes("src/actions/cart.ts#addToCart") ?? false;
226
+ import { addToCart } from "./actions/cart";
227
+
228
+ export const revalidateCartData = (ctx) => ctx.isAction(addToCart) || undefined;
227
229
  ```
228
230
 
229
231
  ```typescript
@@ -243,9 +245,10 @@ You can also package them as importable handoff helpers:
243
245
  ```typescript
244
246
  // revalidation-contracts.ts
245
247
  import { revalidate } from "@rangojs/router";
248
+ import * as AuthActions from "./actions/auth";
246
249
 
247
- export const revalidateAuthData = ({ actionId }) =>
248
- actionId?.includes("src/actions/auth.ts#") ?? false;
250
+ export const revalidateAuthData = (ctx) =>
251
+ ctx.isAction(AuthActions) || undefined;
249
252
  export const revalidateAuth = () => [revalidate(revalidateAuthData)];
250
253
  ```
251
254
 
@@ -263,6 +266,7 @@ layout(<ShellLayout />, () => [
263
266
  ```typescript
264
267
  import { urls } from "@rangojs/router";
265
268
  import { Outlet, ParallelOutlet } from "@rangojs/router/client";
269
+ import * as CartActions from "./actions/cart";
266
270
 
267
271
  function ShopLayout() {
268
272
  return (
@@ -292,7 +296,7 @@ export const shopPatterns = urls(({ path, layout, parallel, loader, revalidate }
292
296
  }, () => [
293
297
  // Layout loaders
294
298
  loader(CartLoader, () => [
295
- revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
299
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined),
296
300
  ]),
297
301
 
298
302
  // Parallel routes
@@ -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