@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c

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 (392) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +126 -16
  3. package/dist/bin/rango.js +319 -95
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +2724 -1053
  6. package/package.json +68 -14
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +64 -2
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +224 -32
  11. package/skills/caching/SKILL.md +279 -17
  12. package/skills/composability/SKILL.md +27 -3
  13. package/skills/css/SKILL.md +76 -0
  14. package/skills/debug-manifest/SKILL.md +4 -2
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +11 -9
  17. package/skills/hooks/SKILL.md +243 -29
  18. package/skills/host-router/SKILL.md +83 -23
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +68 -19
  21. package/skills/layout/SKILL.md +13 -9
  22. package/skills/links/SKILL.md +190 -23
  23. package/skills/loader/SKILL.md +235 -9
  24. package/skills/middleware/SKILL.md +18 -10
  25. package/skills/migrate-nextjs/SKILL.md +43 -19
  26. package/skills/migrate-react-router/SKILL.md +8 -2
  27. package/skills/mime-routes/SKILL.md +28 -1
  28. package/skills/observability/SKILL.md +172 -0
  29. package/skills/parallel/SKILL.md +18 -7
  30. package/skills/prerender/SKILL.md +65 -60
  31. package/skills/rango/SKILL.md +251 -24
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +115 -48
  34. package/skills/route/SKILL.md +46 -5
  35. package/skills/router-setup/SKILL.md +30 -8
  36. package/skills/scripts/SKILL.md +179 -0
  37. package/skills/server-actions/SKILL.md +775 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +130 -0
  40. package/skills/testing/bindings.md +103 -0
  41. package/skills/testing/cache-prerender.md +127 -0
  42. package/skills/testing/client-components.md +124 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +91 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +122 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +123 -0
  54. package/skills/typesafety/SKILL.md +322 -29
  55. package/skills/use-cache/SKILL.md +57 -14
  56. package/skills/view-transitions/SKILL.md +337 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +0 -65
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/connection-warmup.ts +134 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +192 -150
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/navigation-bridge.ts +94 -25
  70. package/src/browser/navigation-client.ts +121 -84
  71. package/src/browser/navigation-store-handle.ts +38 -0
  72. package/src/browser/navigation-store.ts +115 -67
  73. package/src/browser/navigation-transaction.ts +9 -59
  74. package/src/browser/network-error-handler.ts +34 -7
  75. package/src/browser/partial-update.ts +147 -128
  76. package/src/browser/prefetch/cache.ts +107 -56
  77. package/src/browser/prefetch/fetch.ts +204 -34
  78. package/src/browser/prefetch/queue.ts +6 -3
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +30 -7
  81. package/src/browser/react/NavigationProvider.tsx +283 -118
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  84. package/src/browser/react/filter-segment-order.ts +66 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +178 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +17 -14
  90. package/src/browser/react/use-href.tsx +8 -1
  91. package/src/browser/react/use-link-status.ts +33 -8
  92. package/src/browser/react/use-navigation.ts +10 -5
  93. package/src/browser/react/use-params.ts +11 -11
  94. package/src/browser/react/use-reverse.ts +106 -0
  95. package/src/browser/react/use-router.ts +25 -3
  96. package/src/browser/react/use-search-params.ts +0 -5
  97. package/src/browser/react/use-segments.ts +11 -21
  98. package/src/browser/response-adapter.ts +99 -8
  99. package/src/browser/rsc-router.tsx +91 -24
  100. package/src/browser/scroll-restoration.ts +30 -17
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +214 -55
  103. package/src/browser/types.ts +80 -9
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +60 -35
  107. package/src/build/generate-route-types.ts +2 -1
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +117 -14
  111. package/src/build/route-types/ast-route-extraction.ts +15 -8
  112. package/src/build/route-types/codegen.ts +16 -5
  113. package/src/build/route-types/include-resolution.ts +117 -23
  114. package/src/build/route-types/param-extraction.ts +6 -3
  115. package/src/build/route-types/per-module-writer.ts +22 -6
  116. package/src/build/route-types/router-processing.ts +55 -28
  117. package/src/build/route-types/scan-filter.ts +1 -1
  118. package/src/build/route-types/source-scan.ts +216 -0
  119. package/src/build/runtime-discovery.ts +9 -20
  120. package/src/cache/cache-error.ts +104 -0
  121. package/src/cache/cache-key-utils.ts +29 -13
  122. package/src/cache/cache-policy.ts +108 -34
  123. package/src/cache/cache-runtime.ts +224 -41
  124. package/src/cache/cache-scope.ts +188 -82
  125. package/src/cache/cache-tag.ts +103 -0
  126. package/src/cache/cf/cf-base64.ts +33 -0
  127. package/src/cache/cf/cf-cache-constants.ts +127 -0
  128. package/src/cache/cf/cf-cache-store.ts +1989 -378
  129. package/src/cache/cf/cf-cache-types.ts +349 -0
  130. package/src/cache/cf/cf-kv-utils.ts +46 -0
  131. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  132. package/src/cache/cf/index.ts +6 -16
  133. package/src/cache/document-cache.ts +89 -21
  134. package/src/cache/handle-snapshot.ts +70 -0
  135. package/src/cache/index.ts +10 -20
  136. package/src/cache/memory-segment-store.ts +136 -37
  137. package/src/cache/profile-registry.ts +46 -31
  138. package/src/cache/read-through-swr.ts +56 -12
  139. package/src/cache/segment-codec.ts +9 -17
  140. package/src/cache/tag-invalidation.ts +230 -0
  141. package/src/cache/types.ts +37 -100
  142. package/src/client.rsc.tsx +44 -21
  143. package/src/client.tsx +36 -61
  144. package/src/cloudflare/index.ts +11 -0
  145. package/src/cloudflare/tracing.ts +109 -0
  146. package/src/component-utils.ts +19 -0
  147. package/src/components/DefaultDocument.tsx +8 -2
  148. package/src/context-var.ts +18 -6
  149. package/src/decode-loader-results.ts +52 -0
  150. package/src/defer.ts +196 -0
  151. package/src/deps/ssr.ts +0 -1
  152. package/src/encode-kv.ts +49 -0
  153. package/src/errors.ts +30 -4
  154. package/src/escape-script.ts +52 -0
  155. package/src/handle.ts +31 -23
  156. package/src/handles/MetaTags.tsx +62 -19
  157. package/src/handles/Scripts.tsx +183 -0
  158. package/src/handles/breadcrumbs.ts +37 -8
  159. package/src/handles/is-thenable.ts +19 -0
  160. package/src/handles/meta.ts +51 -40
  161. package/src/handles/script.ts +244 -0
  162. package/src/host/cookie-handler.ts +9 -60
  163. package/src/host/errors.ts +0 -24
  164. package/src/host/index.ts +8 -2
  165. package/src/host/pattern-matcher.ts +23 -52
  166. package/src/host/router.ts +107 -99
  167. package/src/host/testing.ts +40 -27
  168. package/src/host/types.ts +37 -4
  169. package/src/host/utils.ts +1 -1
  170. package/src/href-client.ts +137 -22
  171. package/src/index.rsc.ts +96 -12
  172. package/src/index.ts +94 -14
  173. package/src/internal-debug.ts +11 -10
  174. package/src/loader-store.ts +500 -0
  175. package/src/loader.rsc.ts +20 -13
  176. package/src/loader.ts +12 -11
  177. package/src/missing-id-error.ts +68 -0
  178. package/src/outlet-context.ts +1 -1
  179. package/src/outlet-provider.tsx +1 -5
  180. package/src/prerender/param-hash.ts +16 -16
  181. package/src/prerender/store.ts +32 -37
  182. package/src/prerender.ts +61 -6
  183. package/src/redirect-origin.ts +100 -0
  184. package/src/regex-escape.ts +8 -0
  185. package/src/render-error-thrower.tsx +20 -0
  186. package/src/response-utils.ts +34 -0
  187. package/src/reverse.ts +65 -40
  188. package/src/root-error-boundary.tsx +1 -19
  189. package/src/route-content-wrapper.tsx +19 -77
  190. package/src/route-definition/dsl-helpers.ts +304 -309
  191. package/src/route-definition/helper-factories.ts +28 -140
  192. package/src/route-definition/helpers-types.ts +82 -55
  193. package/src/route-definition/index.ts +1 -2
  194. package/src/route-definition/redirect.ts +44 -11
  195. package/src/route-definition/resolve-handler-use.ts +12 -1
  196. package/src/route-definition/use-item-types.ts +29 -0
  197. package/src/route-map-builder.ts +0 -16
  198. package/src/route-types.ts +19 -46
  199. package/src/router/basename.ts +14 -0
  200. package/src/router/content-negotiation.ts +73 -25
  201. package/src/router/error-handling.ts +45 -18
  202. package/src/router/find-match.ts +44 -23
  203. package/src/router/handler-context.ts +27 -43
  204. package/src/router/instrument.ts +350 -0
  205. package/src/router/intercept-resolution.ts +39 -20
  206. package/src/router/lazy-includes.ts +10 -47
  207. package/src/router/loader-resolution.ts +155 -72
  208. package/src/router/logging.ts +0 -6
  209. package/src/router/manifest.ts +18 -29
  210. package/src/router/match-api.ts +9 -24
  211. package/src/router/match-context.ts +0 -22
  212. package/src/router/match-handlers.ts +58 -58
  213. package/src/router/match-middleware/background-revalidation.ts +40 -24
  214. package/src/router/match-middleware/cache-lookup.ts +159 -285
  215. package/src/router/match-middleware/cache-store.ts +64 -52
  216. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  217. package/src/router/match-middleware/segment-resolution.ts +0 -22
  218. package/src/router/match-pipelines.ts +1 -42
  219. package/src/router/match-result.ts +44 -74
  220. package/src/router/metrics.ts +0 -34
  221. package/src/router/middleware-types.ts +7 -134
  222. package/src/router/middleware.ts +247 -166
  223. package/src/router/navigation-snapshot.ts +0 -51
  224. package/src/router/params-util.ts +23 -0
  225. package/src/router/pattern-matching.ts +85 -94
  226. package/src/router/prefetch-cache-ttl.ts +51 -0
  227. package/src/router/prerender-match.ts +104 -65
  228. package/src/router/preview-match.ts +3 -1
  229. package/src/router/request-classification.ts +28 -62
  230. package/src/router/revalidation.ts +123 -73
  231. package/src/router/route-snapshot.ts +0 -1
  232. package/src/router/router-context.ts +3 -28
  233. package/src/router/router-interfaces.ts +83 -35
  234. package/src/router/router-options.ts +136 -5
  235. package/src/router/router-registry.ts +2 -5
  236. package/src/router/segment-resolution/fresh.ts +97 -84
  237. package/src/router/segment-resolution/helpers.ts +86 -6
  238. package/src/router/segment-resolution/loader-cache.ts +76 -39
  239. package/src/router/segment-resolution/revalidation.ts +272 -320
  240. package/src/router/segment-resolution/static-store.ts +19 -5
  241. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  242. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  243. package/src/router/segment-resolution.ts +5 -1
  244. package/src/router/segment-wrappers.ts +6 -5
  245. package/src/router/state-cookie-name.ts +33 -0
  246. package/src/router/substitute-pattern-params.ts +56 -0
  247. package/src/router/telemetry-otel.ts +161 -199
  248. package/src/router/telemetry.ts +96 -19
  249. package/src/router/timeout.ts +0 -20
  250. package/src/router/tracing.ts +206 -0
  251. package/src/router/trie-matching.ts +162 -64
  252. package/src/router/types.ts +9 -63
  253. package/src/router/url-params.ts +0 -5
  254. package/src/router.ts +110 -55
  255. package/src/rsc/handler-context.ts +3 -2
  256. package/src/rsc/handler.ts +264 -220
  257. package/src/rsc/helpers.ts +100 -6
  258. package/src/rsc/index.ts +2 -5
  259. package/src/rsc/json-route-result.ts +38 -0
  260. package/src/rsc/loader-fetch.ts +114 -38
  261. package/src/rsc/manifest-init.ts +28 -41
  262. package/src/rsc/origin-guard.ts +39 -25
  263. package/src/rsc/progressive-enhancement.ts +117 -11
  264. package/src/rsc/redirect-guard.ts +99 -0
  265. package/src/rsc/response-cache-serve.ts +238 -0
  266. package/src/rsc/response-error.ts +79 -12
  267. package/src/rsc/response-route-handler.ts +88 -188
  268. package/src/rsc/rsc-rendering.ts +98 -76
  269. package/src/rsc/runtime-warnings.ts +23 -10
  270. package/src/rsc/server-action.ts +281 -117
  271. package/src/rsc/ssr-setup.ts +16 -0
  272. package/src/rsc/transition-gate.ts +89 -0
  273. package/src/rsc/types.ts +23 -5
  274. package/src/runtime-env.ts +18 -0
  275. package/src/search-params.ts +35 -30
  276. package/src/segment-loader-promise.ts +31 -4
  277. package/src/segment-system.tsx +254 -143
  278. package/src/serialize.ts +243 -0
  279. package/src/server/context.ts +163 -51
  280. package/src/server/cookie-parse.ts +32 -0
  281. package/src/server/cookie-store.ts +80 -5
  282. package/src/server/handle-store.ts +21 -38
  283. package/src/server/loader-registry.ts +33 -42
  284. package/src/server/request-context.ts +287 -178
  285. package/src/ssr/index.tsx +21 -16
  286. package/src/static-handler.ts +10 -13
  287. package/src/testing/cache-status.ts +162 -0
  288. package/src/testing/collect-handle.ts +40 -0
  289. package/src/testing/dispatch.ts +701 -0
  290. package/src/testing/dom.entry.ts +22 -0
  291. package/src/testing/e2e/fixture.ts +188 -0
  292. package/src/testing/e2e/index.ts +128 -0
  293. package/src/testing/e2e/matchers.ts +35 -0
  294. package/src/testing/e2e/page-helpers.ts +272 -0
  295. package/src/testing/e2e/parity.ts +387 -0
  296. package/src/testing/e2e/server.ts +195 -0
  297. package/src/testing/flight-matchers.ts +97 -0
  298. package/src/testing/flight-normalize.ts +11 -0
  299. package/src/testing/flight-runtime.d.ts +57 -0
  300. package/src/testing/flight-tree.ts +682 -0
  301. package/src/testing/flight.entry.ts +52 -0
  302. package/src/testing/flight.ts +257 -0
  303. package/src/testing/generated-routes.ts +183 -0
  304. package/src/testing/index.ts +105 -0
  305. package/src/testing/internal/context.ts +371 -0
  306. package/src/testing/internal/flight-client-globals.ts +30 -0
  307. package/src/testing/internal/seed-vars.ts +54 -0
  308. package/src/testing/render-handler.ts +357 -0
  309. package/src/testing/render-route.tsx +581 -0
  310. package/src/testing/run-loader.ts +385 -0
  311. package/src/testing/run-middleware.ts +205 -0
  312. package/src/testing/run-transition-when.ts +164 -0
  313. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  314. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  315. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  316. package/src/testing/vitest-stubs/version.ts +5 -0
  317. package/src/testing/vitest.ts +305 -0
  318. package/src/theme/ThemeProvider.tsx +20 -58
  319. package/src/theme/ThemeScript.tsx +7 -9
  320. package/src/theme/constants.ts +52 -13
  321. package/src/theme/index.ts +0 -7
  322. package/src/theme/theme-context.ts +1 -5
  323. package/src/theme/theme-script.ts +22 -21
  324. package/src/theme/use-theme.ts +0 -3
  325. package/src/types/boundaries.ts +0 -35
  326. package/src/types/cache-types.ts +13 -4
  327. package/src/types/error-types.ts +30 -90
  328. package/src/types/global-namespace.ts +54 -41
  329. package/src/types/handler-context.ts +110 -62
  330. package/src/types/index.ts +3 -10
  331. package/src/types/loader-types.ts +11 -9
  332. package/src/types/request-scope.ts +112 -0
  333. package/src/types/route-config.ts +6 -50
  334. package/src/types/route-entry.ts +0 -6
  335. package/src/types/segments.ts +135 -14
  336. package/src/urls/include-helper.ts +9 -56
  337. package/src/urls/index.ts +1 -11
  338. package/src/urls/path-helper-types.ts +29 -12
  339. package/src/urls/path-helper.ts +17 -106
  340. package/src/urls/pattern-types.ts +36 -19
  341. package/src/urls/response-types.ts +22 -29
  342. package/src/urls/type-extraction.ts +58 -139
  343. package/src/urls/urls-function.ts +1 -19
  344. package/src/use-loader.tsx +292 -107
  345. package/src/vite/debug.ts +185 -0
  346. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  347. package/src/vite/discovery/discover-routers.ts +126 -85
  348. package/src/vite/discovery/discovery-errors.ts +194 -0
  349. package/src/vite/discovery/gate-state.ts +171 -0
  350. package/src/vite/discovery/prerender-collection.ts +96 -68
  351. package/src/vite/discovery/route-types-writer.ts +40 -84
  352. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  353. package/src/vite/discovery/state.ts +44 -0
  354. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  355. package/src/vite/index.ts +2 -0
  356. package/src/vite/inject-client-debug.ts +36 -0
  357. package/src/vite/plugin-types.ts +126 -8
  358. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  359. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  360. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  361. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  362. package/src/vite/plugins/expose-action-id.ts +48 -95
  363. package/src/vite/plugins/expose-id-utils.ts +88 -55
  364. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  365. package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
  366. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  367. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  368. package/src/vite/plugins/expose-internal-ids.ts +505 -486
  369. package/src/vite/plugins/performance-tracks.ts +26 -25
  370. package/src/vite/plugins/refresh-cmd.ts +1 -1
  371. package/src/vite/plugins/use-cache-transform.ts +73 -83
  372. package/src/vite/plugins/version-injector.ts +40 -29
  373. package/src/vite/plugins/version-plugin.ts +37 -40
  374. package/src/vite/plugins/virtual-entries.ts +39 -25
  375. package/src/vite/rango.ts +109 -118
  376. package/src/vite/router-discovery.ts +718 -119
  377. package/src/vite/utils/ast-handler-extract.ts +26 -35
  378. package/src/vite/utils/banner.ts +1 -1
  379. package/src/vite/utils/bundle-analysis.ts +10 -15
  380. package/src/vite/utils/client-chunks.ts +184 -0
  381. package/src/vite/utils/directive-prologue.ts +40 -0
  382. package/src/vite/utils/forward-user-plugins.ts +171 -0
  383. package/src/vite/utils/manifest-utils.ts +4 -59
  384. package/src/vite/utils/package-resolution.ts +20 -52
  385. package/src/vite/utils/prerender-utils.ts +54 -39
  386. package/src/vite/utils/shared-utils.ts +90 -41
  387. package/src/browser/action-response-classifier.ts +0 -99
  388. package/src/browser/react/use-client-cache.ts +0 -58
  389. package/src/browser/shallow.ts +0 -40
  390. package/src/handles/index.ts +0 -7
  391. package/src/network-error-thrower.tsx +0 -23
  392. package/src/router/middleware-cookies.ts +0 -55
@@ -0,0 +1,123 @@
1
+ # Testing setup — the two vitest projects
2
+
3
+ **Layer:** cross-cutting (vitest config) · **Import:** `@rangojs/router/testing/vitest`
4
+
5
+ Real machinery: Vite transpiles `@rangojs/router`'s shipped TS source and resolves the bare specifier to its react-server impls so your app's router / loaders / middleware import in a bare Vitest process. You SEED nothing here — this file only wires the two projects every other recipe builds on. The node/DOM project keeps React on its CLIENT build; the Flight project flips to the `react-server` condition.
6
+
7
+ ## API
8
+
9
+ ### Options — `RangoTestAliasOptions`
10
+
11
+ | Field | Type | Meaning |
12
+ | -------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `preset` | `"node" \| "cloudflare"` | Deployment preset, matching `rango({ preset })` in the Vite plugin. `"cloudflare"` additionally stubs the `cloudflare:workers` / `cloudflare:email` runtime virtuals a CF route tree imports. A string (not a boolean) so more presets can be added without an API change. Default `"node"`. |
14
+
15
+ ### Functions
16
+
17
+ | Function | Returns | Use |
18
+ | --------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
19
+ | `rangoTestConfig(opts?)` | `{ alias, server: { deps: { inline } } }` | Recommended. Spread into the node/DOM project's `test` block. Bundles the resolve aliases AND `server.deps.inline`. |
20
+ | `rangoTestAliases(opts?)` | `TestAlias[]` (`{ find, replacement }[]`) | Lower-level. The bare `@rangojs/router` -> `index.rsc.ts` alias plus the `:version` / `@vitejs/plugin-rsc/rsc` stubs (and CF stubs under `preset:"cloudflare"`). Used in the rsc project's `resolve.alias`. |
21
+ | `rangoUseClientTransform()` | a Vite plugin (`{ name, transform }`) | Add to the rsc project `plugins`. Applies the `"use client"` transform so `renderServerTree` auto-discovers client islands from the server tree's imports. |
22
+
23
+ ### Returns — `RangoTestConfig` (from `rangoTestConfig`)
24
+
25
+ ```ts
26
+ interface RangoTestConfig {
27
+ alias: TestAlias[]; // -> test.alias
28
+ server: { deps: { inline: RegExp[] } }; // [/@rangojs[/\\]router/] -> test.server.deps.inline
29
+ }
30
+ ```
31
+
32
+ ## Recipe
33
+
34
+ ```ts
35
+ // vitest.config.ts — the node + DOM project (keeps React on its CLIENT build)
36
+ import { defineConfig } from "vitest/config";
37
+ import { rangoTestConfig } from "@rangojs/router/testing/vitest";
38
+
39
+ export default defineConfig({
40
+ test: {
41
+ globals: true,
42
+ include: ["test/**/*.test.{ts,tsx}"],
43
+ environment: "node", // renderRoute tests add a `// @vitest-environment happy-dom` pragma
44
+ // `preset: "cloudflare"` also stubs cloudflare:workers / cloudflare:email (default "node").
45
+ ...rangoTestConfig({ preset: "cloudflare" }),
46
+ },
47
+ });
48
+ ```
49
+
50
+ ```ts
51
+ // vitest.rsc.config.ts — the Flight project (react-server condition)
52
+ import { defineConfig } from "vitest/config";
53
+ import {
54
+ rangoTestAliases,
55
+ rangoUseClientTransform,
56
+ rangoInlineDeps,
57
+ } from "@rangojs/router/testing/vitest";
58
+
59
+ // Production React in this process AND any forked worker (forks inherit env).
60
+ process.env.NODE_ENV = "production";
61
+
62
+ export default defineConfig({
63
+ plugins: [rangoUseClientTransform()],
64
+ resolve: {
65
+ conditions: ["react-server"],
66
+ alias: rangoTestAliases({ preset: "cloudflare" }), // or { preset: "node" }
67
+ },
68
+ test: {
69
+ globals: true,
70
+ include: ["**/*.rsc-test.{ts,tsx}"],
71
+ pool: "forks",
72
+ execArgv: ["--conditions=react-server"], // or React throws "react-server condition must be enabled"
73
+ // Required for an installed consumer on Node >= 23 (rango ships TS source).
74
+ server: { deps: { inline: rangoInlineDeps } },
75
+ },
76
+ });
77
+ ```
78
+
79
+ ```ts
80
+ // example.test.ts — one test in the node/DOM project, importing real app code
81
+ import { describe, it, expect } from "vitest";
82
+ import { dispatch } from "@rangojs/router/testing";
83
+ import { createRouter } from "@rangojs/router";
84
+ import { apiPatterns } from "../src/api/urls"; // path.json(...) routes only, no Prerender()
85
+
86
+ const router = createRouter().routes(apiPatterns);
87
+
88
+ describe("api", () => {
89
+ it("serializes a JSON response route", async () => {
90
+ const res = await dispatch(router, { request: "/health" });
91
+ expect(res.status).toBe(200);
92
+ expect(await res.json()).toEqual({ status: "ok" });
93
+ });
94
+ });
95
+ ```
96
+
97
+ Scripts:
98
+
99
+ ```jsonc
100
+ {
101
+ "scripts": {
102
+ "test:unit": "vitest run",
103
+ "test:unit:rsc": "vitest run --config vitest.rsc.config.ts",
104
+ },
105
+ }
106
+ ```
107
+
108
+ ## Caveats
109
+
110
+ - Node >= 23 requires `rangoTestConfig()`, not bare `rangoTestAliases()`. `@rangojs/router` is consumed as SOURCE (exports -> `./src/*.ts`), and Node >= 23 refuses to type-strip `.ts` under `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`). `rangoTestConfig` ships as compiled JS AND adds `server.deps.inline: [/@rangojs[/\\]router/]` so Vite (not Node) transpiles rango source. With bare `rangoTestAliases` you must wire `deps.inline` yourself.
111
+ - Two separate projects. The node/DOM project keeps React on its CLIENT build; the Flight project uses the `react-server` condition in a separate `vitest.rsc.config.ts`. The main project must NOT set `react-server` — it flips React to the no-hooks server build and breaks every `renderRoute` / client test.
112
+ - The rsc project needs BOTH `resolve.conditions: ["react-server"]` AND the bare `@rangojs/router` -> `index.rsc.ts` alias from `rangoTestAliases({ preset })`. `resolve.conditions` alone is not reliably applied to bare-package export resolution; without the alias a handler/component reading `getRequestContext()` / `cookies()` resolves the throwing out-of-react-server stub (symptom: `renderHandler` returns `tree: undefined`). `renderToFlightString` / `renderServerTree` now self-diagnose this exact misconfiguration — they reject with an actionable message naming `rangoTestAliases`, rather than surfacing the opaque stub error.
113
+ - `NODE_ENV` must be `"production"` in the rsc project. Dev `NODE_ENV` crashes the bare worker (jsxDEV owner-stack machinery uninitialized) and emits volatile debug rows that defeat stable Flight snapshots.
114
+ - The forked rsc worker (`pool: "forks"`) must force the condition via `execArgv: ["--conditions=react-server"]`, or React throws "the react-server condition must be enabled".
115
+ - The `@rangojs/router:version` and `@vitejs/plugin-rsc/rsc` virtuals must be stubbed; the preset does it. A bare router import without stubbing throws.
116
+ - The rango fragment goes under `test` (`test.alias` + `test.server.deps.inline`, both returned by `rangoTestConfig`), NOT under top-level `resolve`.
117
+ - Wire `rangoUseClientTransform()` into the rsc project `plugins` so islands auto-discover from the server tree imports (see `./server-tree.md`); without it, register islands explicitly with `clientComponents`.
118
+
119
+ ## See also
120
+
121
+ - (cross-cutting)
122
+ - Siblings: `./flight.md`, `./server-tree.md`, `./render-handler.md`, `./response-routes.md`
123
+ - Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "Setup" (and the subsections "Resolving @rangojs/router in a unit test — use the preset" and "Two vitest projects")
@@ -32,12 +32,34 @@ is only needed when you want the richer `typeof router.routeMap` shape
32
32
  available globally.
33
33
 
34
34
  - `GeneratedRouteMap` — auto-registered by `router.named-routes.gen.ts`
35
- Use for `Handler<"name">`, `Prerender<"name">`, server `ctx.reverse()`,
36
- and named-route param/search inference.
35
+ Use for `Handler<"name">` (type annotation), `Prerender<"name">(...)` (function
36
+ call with type arg for param inference), server `ctx.reverse()`, and
37
+ named-route param/search inference.
37
38
  - `typeof router.routeMap` — the real merged route map from your router
38
39
  instance, including response-route metadata such as `{ path, response }`.
39
40
  - `RegisteredRoutes` — manual global hook for exposing `typeof router.routeMap`
40
- to utilities like `href()`, `ValidPaths`, and `PathResponse`.
41
+ to global utilities that need the exact router-builder map, especially
42
+ `Rango.PathResponse`.
43
+
44
+ ### Generated Route Type Surfaces
45
+
46
+ There are three distinct typing surfaces. They are **not** interchangeable —
47
+ pick the one that matches what you need to type:
48
+
49
+ | Surface | Source | Scope | Gives | Does not give |
50
+ | ------------------- | ---------------------------------------- | ------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------ |
51
+ | `GeneratedRouteMap` | `router.named-routes.gen.ts` (auto) | global | route names, path params, search schemas | response/MIME payloads |
52
+ | `routes` | per-module `*.gen.ts` (`rango generate`) | local | local names, params, search | the global app map |
53
+ | `RegisteredRoutes` | manual `extends typeof router.routeMap` | global | paths, params, **response payloads** | the `Handler`/`Prerender` default (those read `GeneratedRouteMap` to avoid a `router.tsx` cycle) |
54
+
55
+ Key consequence: `href()` and the ambient `Rango.Path` type are typed from
56
+ whichever map is present — they prefer `RegisteredRoutes` when you wire it, otherwise fall back to
57
+ the auto-generated `GeneratedRouteMap`, so **`rango generate` alone gives you
58
+ path-checked `href()`** with no manual augmentation. Response and MIME payload
59
+ inference is the exception: it comes only from `typeof router.routeMap` (via
60
+ `RegisteredRoutes`), because `GeneratedRouteMap` carries paths + search but no
61
+ payloads — so `Rango.PathResponse` resolves to `never` until you wire
62
+ `RegisteredRoutes`.
41
63
 
42
64
  Recommended setup:
43
65
 
@@ -50,7 +72,7 @@ import type { AppBindings, AppVars } from "./env";
50
72
  export const router = createRouter<AppBindings>({}).routes(urlpatterns);
51
73
 
52
74
  declare global {
53
- namespace RSCRouter {
75
+ namespace Rango {
54
76
  interface Env extends AppBindings {}
55
77
  interface Vars extends AppVars {}
56
78
  interface RegisteredRoutes extends typeof router.routeMap {}
@@ -58,6 +80,54 @@ declare global {
58
80
  }
59
81
  ```
60
82
 
83
+ ### Single-App Setup Checklist
84
+
85
+ For one app, keep the ambient types, generated named-routes file, and router
86
+ instance in the same TypeScript program:
87
+
88
+ ```jsonc
89
+ // tsconfig.json
90
+ {
91
+ "compilerOptions": {
92
+ "strict": true,
93
+ "moduleResolution": "bundler",
94
+ "jsx": "react-jsx",
95
+ "noEmit": true,
96
+ },
97
+ "include": ["src"],
98
+ "files": ["src/router.tsx"],
99
+ }
100
+ ```
101
+
102
+ Then generate the route types from the router file:
103
+
104
+ ```bash
105
+ npx rango generate src/router.tsx
106
+ ```
107
+
108
+ This creates `src/router.named-routes.gen.ts`, which augments
109
+ `Rango.GeneratedRouteMap`. Keep that generated file committed with the router
110
+ source. The `files` entry keeps `router.tsx` in the program even when nothing
111
+ imports it directly, so `Rango.Env`, `Rango.Vars`, and optional
112
+ `Rango.RegisteredRoutes` augmentation are visible to handlers, loaders, actions,
113
+ and client helpers.
114
+
115
+ ### Named Routes, `$$routeNames`, And `router.routeMap`
116
+
117
+ There are two runtime/type surfaces with similar names:
118
+
119
+ - `router.named-routes.gen.ts` exports `NamedRoutes` and augments
120
+ `Rango.GeneratedRouteMap`. The Vite plugin imports that file internally and
121
+ injects it as `$$routeNames` so `router.reverse` has the static route-name map.
122
+ App code should not pass or import `$$routeNames` directly.
123
+ - `router.routeMap` is the public router instance property for type extraction.
124
+ Use `typeof router.routeMap` when augmenting `Rango.RegisteredRoutes` for
125
+ global response payload helpers such as `Rango.PathResponse`.
126
+
127
+ Do not document or use a public `router.routeNames` API unless one is
128
+ intentionally added. Today, the public extraction surface is `router.routeMap`;
129
+ the generated file and `$$routeNames` are build machinery.
130
+
61
131
  ## Route Definition with Type-Safe Names
62
132
 
63
133
  ```typescript
@@ -127,17 +197,107 @@ function ShopNav() {
127
197
  }
128
198
  ```
129
199
 
130
- `href()` and path-based response utilities read from `RegisteredRoutes`, so if
131
- you want them typed globally you should augment:
200
+ `href()` and the `Rango.Path` type read from `RegisteredRoutes` when you augment
201
+ it, otherwise from the auto-generated `GeneratedRouteMap` so `rango generate`
202
+ alone type-checks `href()` paths with no manual augmentation. The augmentation
203
+ below is only needed for **`Rango.PathResponse`** (response-payload inference), which
204
+ `GeneratedRouteMap` cannot provide:
132
205
 
133
206
  ```typescript
134
207
  declare global {
135
- namespace RSCRouter {
208
+ namespace Rango {
136
209
  interface RegisteredRoutes extends typeof router.routeMap {}
137
210
  }
138
211
  }
139
212
  ```
140
213
 
214
+ For wrapper helpers, type the path parameter as `Rango.Path`. It is ambient (no
215
+ import) and shares `href()`'s compile-time path checking, so a wrapper stays in
216
+ sync with your routes automatically:
217
+
218
+ ```typescript
219
+ import { href } from "@rangojs/router/client";
220
+
221
+ export const appHref = (path: Rango.Path): string => href(path);
222
+ ```
223
+
224
+ For response-route payloads, `Rango.PathResponse<T>` is the ambient lookup. It
225
+ accepts a route _pattern_ **or** a concrete path, so it also serves as the return
226
+ type of a typed `fetch` wrapper. It only resolves once `RegisteredRoutes` carries
227
+ response metadata:
228
+
229
+ ```typescript
230
+ import { href } from "@rangojs/router/client";
231
+
232
+ type Product = Rango.PathResponse<"/api/products/:id">; // by pattern
233
+ type Same = Rango.PathResponse<"/api/products/42">; // by concrete path
234
+
235
+ // Response inferred from the concrete path passed in:
236
+ async function get<T extends Rango.Path>(
237
+ path: T,
238
+ ): Promise<Rango.PathResponse<T>> {
239
+ return fetch(href(path)).then((r) => r.json());
240
+ }
241
+ const product = await get("/api/products/42"); // Product (bare value)
242
+ ```
243
+
244
+ Pattern keys (`/:id`) match exactly; a concrete path under a _nested_ dynamic
245
+ route can match several patterns and union their responses.
246
+
247
+ `Rango.PathResponse` describes the JSON **wire** shape, not the handler's raw
248
+ return. A `path.json()` handler returning `{ createdAt: Date }` resolves here to
249
+ `{ createdAt: string }` (bare value), matching what `r.json()` yields. This
250
+ is applied via the ambient `Rango.JsonSerialize<T>` transform (`Date -> string`,
251
+ honors `toJSON()`, drops functions/`undefined`, `bigint -> never`). A separate
252
+ `Rango.FlightSerialize<T>` models the higher-fidelity RSC Flight boundary
253
+ (loaders / RSC props, where `Date` is preserved) — do **not** use it for
254
+ `path.json()`.
255
+
256
+ ### Overriding serialization globally
257
+
258
+ For your own types, the zero-config way to control the JSON wire shape is a
259
+ `toJSON()` method — `Rango.JsonSerialize` honors it, and it matches the runtime
260
+ exactly (`JSON.stringify` calls `toJSON()`):
261
+
262
+ ```typescript
263
+ class Money {
264
+ constructor(private cents: number) {}
265
+ toJSON(): number {
266
+ return this.cents;
267
+ }
268
+ }
269
+ // Rango.JsonSerialize<Money> is number; Rango.PathResponse reflects it.
270
+ ```
271
+
272
+ To override a transform for types you **don't** own (or for the Flight boundary,
273
+ which has no `toJSON()`), augment its override slot. Because `Rango.JsonSerialize`
274
+ / `Rango.FlightSerialize` are type _aliases_ (TS can't merge those), you provide a
275
+ single member that is your **complete** transform, delegating to the built-in for
276
+ the cases you don't change:
277
+
278
+ ```typescript
279
+ declare global {
280
+ namespace Rango {
281
+ interface JsonSerializeOverride<T> {
282
+ app: T extends Decimal ? string : Rango.JsonSerializeBuiltin<T>;
283
+ }
284
+ interface FlightSerializeOverride<T> {
285
+ app: T extends Money ? number : Rango.FlightSerializeBuiltin<T>;
286
+ }
287
+ }
288
+ }
289
+ // Rango.JsonSerialize<Decimal> -> string; Rango.FlightSerialize<Money> -> number;
290
+ // everything else stays on the built-in, recursively (nested fields too).
291
+ ```
292
+
293
+ Rules: provide **exactly one** member (the slot is read as
294
+ `Override<T>[keyof Override<T>]`, so multiple members union and conflict).
295
+ Overrides win over `toJSON()` and apply at every nesting level. Caveat for JSON:
296
+ the `path.json()` runtime is plain `JSON.stringify`, which only honors `toJSON()`,
297
+ so a `JsonSerializeOverride` that disagrees with what the runtime emits will lie —
298
+ prefer `toJSON()` for your own types and use the slot only for types you can't
299
+ modify.
300
+
141
301
  See `/links` for full URL generation guide.
142
302
 
143
303
  ## Environment Type Setup
@@ -155,7 +315,7 @@ export interface AppBindings {
155
315
  AI: Ai;
156
316
  }
157
317
 
158
- // Variables set by middleware — declared via module augmentation
318
+ // Variables set by middleware — declared via global namespace augmentation
159
319
  export interface AppVariables {
160
320
  user?: { id: string; email: string; role: string };
161
321
  requestId?: string;
@@ -175,7 +335,7 @@ const router = createRouter<AppBindings>({
175
335
 
176
336
  // Register bindings and variables globally for implicit typing
177
337
  declare global {
178
- namespace RSCRouter {
338
+ namespace Rango {
179
339
  interface Env extends AppBindings {}
180
340
  interface Vars extends AppVariables {}
181
341
  }
@@ -196,7 +356,7 @@ export const authMiddleware: Middleware = async (ctx, next) => {
196
356
  // loaders - typed context
197
357
  export const UserLoader = createLoader(async (ctx) => {
198
358
  const db = ctx.env.DB; // D1Database (plain bindings)
199
- const userId = ctx.get("user")?.id; // from RSCRouter.Vars
359
+ const userId = ctx.get("user")?.id; // from Rango.Vars
200
360
  return db.prepare("SELECT * FROM users WHERE id = ?").bind(userId).first();
201
361
  });
202
362
  ```
@@ -208,7 +368,7 @@ Register environment types globally for implicit typing:
208
368
  ```typescript
209
369
  // router.tsx
210
370
  declare global {
211
- namespace RSCRouter {
371
+ namespace Rango {
212
372
  interface Env extends AppBindings {}
213
373
  interface Vars extends AppVariables {}
214
374
  }
@@ -220,8 +380,8 @@ Now handlers have typed context without explicit imports:
220
380
  ```typescript
221
381
  // In loaders
222
382
  export const DashboardLoader = createLoader(async (ctx) => {
223
- // ctx.env.DB is typed from global RSCRouter.Env
224
- // ctx.get("user") is typed from global RSCRouter.Vars
383
+ // ctx.env.DB is typed from global Rango.Env
384
+ // ctx.get("user") is typed from global Rango.Vars
225
385
  const user = ctx.get("user");
226
386
  return { user };
227
387
  });
@@ -259,15 +419,21 @@ This avoids circular references because `Handler` defaults to `GeneratedRouteMap
259
419
  (from `router.named-routes.gen.ts`) instead of `RegisteredRoutes` (which depends on `router.tsx`).
260
420
 
261
421
  You can also pass an explicit route map for per-module isolation (opt-in,
262
- after running `npx rango generate`):
422
+ after running `npx rango generate`). With a local map, the route name is
423
+ **dot-prefixed** so params and search resolve from `routes`, not the global map:
263
424
 
264
425
  ```typescript
265
426
  import type { Handler } from "@rangojs/router";
266
427
  import type { routes } from "./urls.gen.js";
267
428
 
268
- export const SearchPage: Handler<"search", routes> = (ctx) => { ... };
429
+ export const SearchPage: Handler<".search", routes> = (ctx) => { ... };
269
430
  ```
270
431
 
432
+ Note the difference: `Handler<"search">` (no dot) resolves against the global
433
+ `GeneratedRouteMap`; `Handler<".search", routes>` resolves against the local
434
+ `routes` map. Mixing them — `Handler<"search", routes>` — silently ignores
435
+ `routes` for param/search inference and only uses it for local `ctx.reverse(".x")`.
436
+
271
437
  Supported types: `"string"`, `"number"`, `"boolean"`, with `?` suffix for optional.
272
438
  Values are automatically coerced from query string (e.g., `"2"` becomes `2` for numbers).
273
439
  Routes without a `search` schema keep the standard `URLSearchParams` behavior.
@@ -287,6 +453,12 @@ type SP = RouteSearchParams<"search">;
287
453
  type P = RouteParams<"blogPost">;
288
454
  // { slug: string }
289
455
 
456
+ // Optional URL params (`:slug?`) resolve to `string | undefined`
457
+ // because absent segments are omitted from `ctx.params` at runtime.
458
+ type C = RouteParams<"checkout">;
459
+ // { step?: string }
460
+ // → ctx.params.step is `string | undefined`; use `?? "default"` to coalesce.
461
+
290
462
  // Use in component props
291
463
  interface SearchResultsProps {
292
464
  params: RouteSearchParams<"search">;
@@ -319,6 +491,19 @@ export const NamedRoutes = {
319
491
  } as const;
320
492
  ```
321
493
 
494
+ You never open a `.gen.ts` by hand. Treat the generated types as call-site
495
+ honesty checks, not modules to read:
496
+
497
+ - **Do not import `router.named-routes.gen.ts` directly**, and don't reach for
498
+ `Rango.GeneratedRouteMap`. It is the whole-app manifest, auto-wired
499
+ globally — `Handler<"name">` and `ctx.reverse("name")` already see it.
500
+ - **Per-module `*.gen.ts` imports are fine** — they are the opt-in local-route
501
+ pattern for `useReverse(routes)` and explicit local handler typing
502
+ (`Handler<".name", routes>`). See `/links`.
503
+
504
+ If a type error points at a generated map instead of your call site, that's a
505
+ smell — fix the call site (or regenerate), never edit the generated file.
506
+
322
507
  ## Loader Type Safety
323
508
 
324
509
  Loaders have typed return values:
@@ -408,9 +593,9 @@ export function PaginationLayout(ctx: any) {
408
593
  }
409
594
  ```
410
595
 
411
- ### Why not just use RSCRouter.Vars?
596
+ ### Why not just use Rango.Vars?
412
597
 
413
- `RSCRouter.Vars` (via module augmentation) provides app-global typing for
598
+ `Rango.Vars` (via global namespace augmentation) provides app-global typing for
414
599
  `ctx.get("key")` / `ctx.set("key", value)`. It works for middleware state
415
600
  shared app-wide. `createVar<T>()` is for route-local or feature-scoped
416
601
  context -- the producer and consumer import the same token, creating a
@@ -462,9 +647,11 @@ export const ProductLoader = createLoader(async (ctx) => {
462
647
  });
463
648
 
464
649
  // Built-in Breadcrumbs — or any custom handle created with createHandle()
650
+ ```
465
651
 
652
+ ```tsx
466
653
  // Client component — typeof infers all generics
467
- ("use client");
654
+ "use client";
468
655
  import { useLoader, useHandle, type Breadcrumbs } from "@rangojs/router/client";
469
656
  import type { ProductLoader } from "../loaders";
470
657
 
@@ -485,6 +672,42 @@ RSC Flight serialization calls `toJSON()` on both loaders and handles,
485
672
  sending only `{ __brand, $$id }` to the client. The hooks recover the
486
673
  full functionality from module-level registries.
487
674
 
675
+ ## Stable identity: `path#export`
676
+
677
+ Loaders, handles, cached functions (`functionId`), and server actions
678
+ (`actionId`) all share one identity scheme: `{modulePath}#{exportName}`,
679
+ injected at build by the `exposeInternalIds` and `exposeActionId` Vite plugins.
680
+ This is also the identity React server actions carry across the Flight boundary,
681
+ which is why a `revalidate()` predicate sees an action as a `path#export` string:
682
+
683
+ ```typescript
684
+ revalidate(
685
+ ({ actionId }) => actionId === "src/actions/cart.ts#addToCart" || undefined,
686
+ );
687
+ ```
688
+
689
+ `actionId` is the only stable reference React exposes across the Flight boundary,
690
+ so it stays as the floor and escape hatch. The hand-written-string surface
691
+ (`actionId?.includes("cart.ts#")`) is brittle: a renamed action or moved file
692
+ silently stops matching with no compile error. Prefer **`ctx.isAction()`** in a
693
+ revalidate predicate — it resolves the action's id from an imported reference, so
694
+ a rename is a type error in one place instead of silent drift:
695
+
696
+ ```ts
697
+ import { addToCart, removeFromCart } from "./actions/cart";
698
+ import * as CartActions from "./actions/cart";
699
+
700
+ revalidate((ctx) => ctx.isAction(addToCart) || undefined); // one action
701
+ revalidate((ctx) => ctx.isAction(addToCart, removeFromCart) || undefined); // several
702
+ revalidate((ctx) => ctx.isAction(CartActions) || undefined); // any action in the module
703
+ ```
704
+
705
+ `ctx.isAction()` (only available on the revalidate predicate's context) returns a
706
+ raw boolean — combine with `|| undefined` for the "revalidate on match, else
707
+ defer" intent. It resolves the reference the same way the router derives
708
+ `actionId` (`$id` in production, `$$id` in dev), so matching
709
+ works in both modes. `actionId` stays available for advanced cases.
710
+
488
711
  ## Location State Type Safety
489
712
 
490
713
  ```typescript
@@ -520,9 +743,37 @@ function ProductHeader() {
520
743
 
521
744
  ## Multi-Project tsconfig Setup
522
745
 
523
- For monorepos or multi-app setups, use a shared base tsconfig. Each app only needs
524
- to extend the base and add its `router.tsx` to `files` so TypeScript picks up the
525
- global type declarations (like `RSCRouter.Env`).
746
+ For monorepos or multi-app setups, each app should have its own TypeScript
747
+ program. Do not typecheck two Rango apps with different `Rango.Env`,
748
+ `Rango.Vars`, or `Rango.RegisteredRoutes` declarations in one tsconfig, because
749
+ ambient global interfaces merge across the whole program.
750
+
751
+ ### Multiple routers in one program
752
+
753
+ `Rango.GeneratedRouteMap` is a **single global interface**. Each router's
754
+ generated `router.named-routes.gen.ts` augments it, so two routers in the **same
755
+ TS program** that define overlapping route names (e.g. both have a `home`) make
756
+ the augmentations collide:
757
+
758
+ ```text
759
+ Interface 'GeneratedRouteMap' cannot simultaneously extend ...
760
+ Named property 'home' ... are not identical.
761
+ ```
762
+
763
+ This is the multi-router / host-router case. Resolve it by:
764
+
765
+ - **Separate TS programs** — give each router its own tsconfig (as below) so only
766
+ one generated map is in scope per program. Recommended.
767
+ - **Unique route-name prefixes** — name routes per router (`appA.home`,
768
+ `appB.home`) so the merged global map has no duplicate keys.
769
+
770
+ A single global generated map is a single-router convenience; global named-route
771
+ typing across multiple routers in one program is not supported today (it would
772
+ need per-router scoping in the generated map).
773
+
774
+ Use a shared base tsconfig for common compiler options, then make every app
775
+ tsconfig include its own source tree, its own `router.tsx`, and the generated
776
+ `router.named-routes.gen.ts` that lives beside that router.
526
777
 
527
778
  ```jsonc
528
779
  // tsconfig.base.json (root)
@@ -561,10 +812,49 @@ global type declarations (like `RSCRouter.Env`).
561
812
  }
562
813
  ```
563
814
 
564
- The `files` array ensures `router.tsx` (which contains `declare global { namespace RSCRouter { interface Env; interface Vars } }`)
565
- is always included in the compilation even if nothing directly imports it. Route types come from the
566
- auto-generated `*.named-routes.gen.ts` file (via `rango generate`), not from manual declaration.
567
- Each app gets its own typed environment without interfering with other apps.
815
+ Run generation per app:
816
+
817
+ ```bash
818
+ npx rango generate apps/shop/src/router.tsx
819
+ npx rango generate apps/blog/src/router.tsx
820
+ ```
821
+
822
+ If an app has multiple tsconfigs (`tsconfig.app.json`, `tsconfig.test.json`,
823
+ `tsconfig.worker.json`), every tsconfig that typechecks Rango handlers,
824
+ components, loaders, actions, or client navigation must see the same app-local
825
+ type surfaces:
826
+
827
+ ```jsonc
828
+ // apps/shop/tsconfig.test.json
829
+ {
830
+ "extends": "./tsconfig.json",
831
+ "include": ["src", "tests"],
832
+ "files": ["src/router.tsx"],
833
+ }
834
+ ```
835
+
836
+ The `files` array ensures `router.tsx` is always included even if nothing
837
+ directly imports it. The generated `router.named-routes.gen.ts` is normally
838
+ covered by `include: ["src"]`; if a tsconfig uses a narrow `include`, add the
839
+ generated file explicitly. Each app gets its own typed environment and named
840
+ route map without interfering with other apps.
841
+
842
+ For response and MIME payload lookup in each app, augment `RegisteredRoutes`
843
+ inside that app's router file:
844
+
845
+ ```typescript
846
+ // apps/shop/src/router.tsx
847
+ export const router = createRouter<ShopEnv>({ document: Document }).routes(
848
+ urlpatterns,
849
+ );
850
+
851
+ declare global {
852
+ namespace Rango {
853
+ interface Env extends ShopEnv {}
854
+ interface RegisteredRoutes extends typeof router.routeMap {}
855
+ }
856
+ }
857
+ ```
568
858
 
569
859
  ## Complete Type-Safe Setup
570
860
 
@@ -600,7 +890,7 @@ const router = createRouter<AppBindings>({
600
890
 
601
891
  // Register bindings and variables globally for implicit typing
602
892
  declare global {
603
- namespace RSCRouter {
893
+ namespace Rango {
604
894
  interface Env extends AppBindings {}
605
895
  interface Vars extends AppVariables {}
606
896
  }
@@ -611,13 +901,16 @@ export default router;
611
901
 
612
902
  // 4. Run `npx rango generate src/router.tsx` to generate
613
903
  // router.named-routes.gen.ts (auto-registers GeneratedRouteMap globally).
614
- // No manual RegisteredRoutes declaration needed.
904
+ // No manual RegisteredRoutes declaration is needed for named-route handlers,
905
+ // ctx.reverse, prerender, href(), or Rango.Path. Add `RegisteredRoutes
906
+ // extends typeof router.routeMap` when global response payload helpers such
907
+ // as Rango.PathResponse need the richer router.routeMap metadata.
615
908
 
616
909
  // 5. loaders/*.ts - Type-safe loaders
617
910
  export const ProductLoader = createLoader(async (ctx) => {
618
911
  // ctx.params: { slug: string }
619
- // ctx.get("user"): User | undefined (from RSCRouter.Vars)
620
- // ctx.env.DB: D1Database (plain bindings from RSCRouter.Env)
912
+ // ctx.get("user"): User | undefined (from Rango.Vars)
913
+ // ctx.env.DB: D1Database (plain bindings from Rango.Env)
621
914
  return { product: await fetchProduct(ctx.params.slug) };
622
915
  });
623
916