@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
@@ -52,8 +52,9 @@ export async function ProductCard({ id }: { id: string }) {
52
52
 
53
53
  ## Named Cache Profiles
54
54
 
55
- Define profiles in createRouter. Profile names map to `"use cache: <name>"` and
56
- `cache('<name>')` in the DSL.
55
+ Define profiles in createRouter. Profile names map to `"use cache: <name>"` in
56
+ the directive. The DSL `cache()` does not accept a string profile name; use an
57
+ options object (`cache({ ttl: 60 })`) or the `"use cache: <name>"` directive.
57
58
 
58
59
  ```typescript
59
60
  createRouter({
@@ -62,13 +63,27 @@ createRouter({
62
63
  short: { ttl: 60, swr: 120 },
63
64
  long: { ttl: 3600, swr: 7200 },
64
65
  products: { ttl: 300, swr: 600, tags: ["products"] },
66
+ // Opt-in: a stale entry re-executes in the foreground during a server
67
+ // action's revalidation render (fresh action response), instead of SWR.
68
+ cms: { ttl: 300, swr: 600, foregroundOnAction: true },
65
69
  },
66
70
  });
67
71
  ```
68
72
 
69
73
  - `"use cache"` (no name) resolves to `default`.
70
74
  - `"use cache: short"` resolves to the `short` profile.
71
- - Unknown profile names throw at build/boot time.
75
+ - `foregroundOnAction: true` (default false): a stale entry serves stale +
76
+ revalidates in the background on a plain navigation (SWR), but re-executes in
77
+ the FOREGROUND during a server action's revalidation render so the action
78
+ response reflects a fresh value (only the store write is deferred). Use it for
79
+ mutation-related cached data; incidental TTL staleness on an ordinary action
80
+ stays SWR so the action is not turned into a synchronous cache-refresh barrier.
81
+ For strong read-your-own-writes after a mutation, prefer `updateTag()` (a hard
82
+ purge, so the action's own re-render is a fresh foreground miss).
83
+ - Unknown profile names throw at runtime, on the first invocation of the cached
84
+ function (the Vite transform does not validate names at build/boot). The error
85
+ is actionable -- it names the missing profile and shows the `createRouter({
86
+ cacheProfiles: { ... } })` entry to add.
72
87
 
73
88
  ## Cache Key
74
89
 
@@ -77,18 +92,33 @@ use-cache:{functionId}:{serializedArgs}
77
92
  ```
78
93
 
79
94
  - `functionId` -- stable ID from Vite transform (module path + export name).
80
- - `serializedArgs` -- non-tainted arguments serialized via RSC `encodeReply()`.
95
+ - `serializedArgs` -- key-generating arguments serialized via RSC `encodeReply()`.
96
+
97
+ When there are no key-generating arguments, the key has no trailing colon -- it is
98
+ just `use-cache:{functionId}`.
81
99
 
82
100
  Different functions always produce different cache keys, even for the same route.
83
101
  This is important for intercepted routes -- the path handler and intercept handler
84
102
  each have their own `functionId` and therefore their own cache entries.
85
103
 
104
+ ### Route context is folded into the key
105
+
106
+ The tainted `ctx` object is excluded from arg serialization (see below), but
107
+ route-identifying fields read off it are extracted into `serializedArgs`:
108
+ `url.host`, route name (`_routeName`), `pathname`, `params`, response type
109
+ (`_responseType`), and the user-facing sorted search params (internal `_rsc*`/`__`
110
+ params excluded). The same cached function called with `ctx` on different routes,
111
+ param combinations, hosts, response types, or query variants therefore produces
112
+ distinct cache entries -- not one shared entry.
113
+
86
114
  ## Tainted Arguments (ctx, env, req)
87
115
 
88
116
  Request-scoped objects are branded with `Symbol.for('rango:nocache')` at creation.
89
117
  When detected:
90
118
 
91
119
  1. **Excluded from cache key** -- request-scoped, not meaningful for keying.
120
+ (The route-identifying fields read off `ctx` are still folded in -- see
121
+ "Route context is folded into the key" above.)
92
122
  2. **Handle data captured on miss** -- side effects via `ctx.use(Handle)` are recorded.
93
123
  3. **Handle data replayed on hit** -- restored into the current request's HandleStore.
94
124
 
@@ -122,12 +152,16 @@ const data = await getCachedData(locale); // locale is now in the cache key
122
152
  These ctx methods **throw** inside a `"use cache"` function because their effects
123
153
  are lost on cache hit (the function body is skipped):
124
154
 
125
- - `ctx.set()` / `ctx.get()` for passing values to children
155
+ - `ctx.set()` for passing values to children
126
156
  - `ctx.header()`
127
157
  - `ctx.setTheme()`
128
158
  - `ctx.setLocationState()`
129
159
  - `ctx.onResponse()`
130
160
 
161
+ `ctx.get()` is **not** exec-guarded inside `"use cache"` -- it is a read, so it is
162
+ safe. (It only throws when reading a non-cacheable variable inside the separate
163
+ route-level `cache()` DSL boundary.)
164
+
131
165
  The error message recommends two alternatives:
132
166
 
133
167
  1. Extract the data fetch into a separate cached function and call ctx methods outside it.
@@ -304,18 +338,27 @@ export async function getProducts() {
304
338
  ## Backing Store
305
339
 
306
340
  Writes to the same `SegmentCacheStore` as `cache()` DSL, `Static()`, and `Prerender()`.
307
- One store, one configuration, one invalidation API. Tag-based invalidation
308
- (`revalidateTag`) works across all mechanisms.
341
+ One store, one configuration.
342
+
343
+ Cache entries (and `cacheProfiles`) can be tagged via `cache({ tags })` or, inside
344
+ a `"use cache"` function, runtime `cacheTag(...tags)`. The built-in
345
+ `MemorySegmentCacheStore` and `CFCacheStore` index by tag. Invalidate on demand
346
+ with `updateTag(...tags)` (awaitable, read-your-own-writes; for server actions) or
347
+ `revalidateTag(...tags)` (background, non-blocking; for route handlers/webhooks).
348
+ Both hard-purge; the difference is awaitability, not stale-serving. For
349
+ `CFCacheStore`, distributed invalidation needs a `kv` namespace (markers live in
350
+ that same namespace). The separate `revalidate()` export is the client-update axis
351
+ (which segments re-render on a navigation or action), not a cache bust.
309
352
 
310
353
  ## Interaction with Other Caching
311
354
 
312
- | Mechanism | Granularity | When | Use case |
313
- | ------------------ | ------------------ | ---------- | ----------------------------------------------- |
314
- | `"use cache"` | Function/component | Runtime | Cache individual data fetches or components |
315
- | `cache()` DSL | Route segment | Runtime | Cache entire route subtrees with children |
316
- | `cache('profile')` | Route segment | Runtime | Same as cache() with a named profile |
317
- | `Static()` | Route segment | Build-time | Render once, never re-render |
318
- | `Prerender()` | Route segment | Build-time | Pre-render known params, optional live fallback |
355
+ | Mechanism | Granularity | When | Use case |
356
+ | -------------------- | ------------------ | ---------- | ----------------------------------------------- |
357
+ | `"use cache"` | Function/component | Runtime | Cache individual data fetches or components |
358
+ | `cache()` DSL | Route segment | Runtime | Cache entire route subtrees with children |
359
+ | `cache({ ttl })` DSL | Route segment | Runtime | Cache a route subtree with explicit options |
360
+ | `Static()` | Route segment | Build-time | Render once, never re-render |
361
+ | `Prerender()` | Route segment | Build-time | Pre-render known params, optional live fallback |
319
362
 
320
363
  ## Dev Mode
321
364
 
@@ -0,0 +1,337 @@
1
+ ---
2
+ name: view-transitions
3
+ description: Configure React View Transitions on layouts, routes, and parallel slots in @rangojs/router
4
+ argument-hint: [layout|route|parallel|intercept]
5
+ ---
6
+
7
+ # View Transitions
8
+
9
+ `transition()` opts a route (or group of routes) into transition-driven navigation. It does two things, and you choose how far to go:
10
+
11
+ 1. **`startTransition` (the foundation).** The navigation commit is driven through React's `startTransition`. That holds the previous content across a same-route navigation (stale-while-revalidate — no loading-skeleton flash) and is the **precondition** for any view-transition animation. Works on **all** React versions.
12
+ 2. **`<ViewTransition>` (the animation, layered on top).** On experimental React, rango also wraps the segment content in React's `<ViewTransition>` so the swap cross-fades/morphs. This is the only part that needs experimental React; pass `viewTransition: false` to keep #1 without it (and place your own `<ViewTransition>` where you want it).
13
+
14
+ > The `<ViewTransition>` layer requires React experimental (the build that exports `<ViewTransition>` / `addTransitionType`). On stable React that layer is a no-op — but the `startTransition` driving (content hold) still applies.
15
+
16
+ ## Purpose: `startTransition` vs `<ViewTransition>`
17
+
18
+ These are two **independent** mechanisms. `startTransition` controls _fallbacks_ (hold the old content vs. flash the Suspense skeleton) and is what lets a view transition fire at all; the `<ViewTransition>` boundary is the _visual cross-fade_.
19
+
20
+ | | `startTransition` **OFF** | `startTransition` **ON** |
21
+ | -------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
22
+ | **`<ViewTransition>` OFF** | plain nav — remount on param change, skeleton flash, no animation | **hold** content (no skeleton flash); a consumer-placed `<ViewTransition>` still morphs; no router cross-fade |
23
+ | **`<ViewTransition>` ON** | **impossible** — React never activates `<ViewTransition>` outside a Transition | hold + router cross-fade |
24
+
25
+ The bottom-left cell is the key constraint: a view transition cannot exist without a `startTransition`. So once you reach for `transition()`, the only real choice is _startTransition_ vs _startTransition + ViewTransition_:
26
+
27
+ | What you want | Config | Effect |
28
+ | -------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
29
+ | nothing (default nav) | no `transition()` | remount + skeleton on param change |
30
+ | `startTransition` only | `transition({ viewTransition: false })` | hold content; place your own `<ViewTransition>` where you want it |
31
+ | `startTransition` + `<ViewTransition>` | `transition({})` / `transition({ enter, exit, … })` | hold + router cross-fade (experimental React; on stable it degrades to the `startTransition`-only row) |
32
+
33
+ `createRouter({ viewTransition: "auto" \| false })` sets the app-wide default for the third row; a per-segment `viewTransition` wins. See [Opting out of the router boundary](#opting-out-of-the-router-boundary-place-your-own-viewtransition) for the full opt-out story.
34
+
35
+ ## What `transition()` does (wrap location)
36
+
37
+ `transition(config)` attaches a [`TransitionConfig`](#transitionconfig) to the surrounding entry. Where the wrap actually lands in the rendered React tree depends on the segment type:
38
+
39
+ | Segment type | Wrap location |
40
+ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41
+ | `layout()` | Around the layout's **default outlet content** (what the layout's `<Outlet />` renders), recursively pushed past nested layouts. Parallel slots (`<ParallelOutlet />`) are siblings of the wrap, not subtree members. |
42
+ | `path()` / `route()` | Around the **route's component itself** (the leaf content). |
43
+ | `parallel()` / `intercept()` slot | `transition()` is accepted by the DSL today, but slot-level rendering does not currently apply a `<ViewTransition>` wrapper. Mount intercept slots in layouts so layout transitions stay scoped to the default outlet. For modal-specific morphs today, use an element-level React `<ViewTransition>` inside the modal component. |
44
+
45
+ The layout case is the important one: stacking a layout transition does **not** wrap the layout chrome (header, sidebar, modal slot); it only morphs whatever flows through that layout's `<Outlet />`.
46
+
47
+ ## Basic Usage
48
+
49
+ A simple cross-fade between pages that share a layout:
50
+
51
+ ```tsx
52
+ import { urls } from "@rangojs/router";
53
+ import { Outlet } from "@rangojs/router/client";
54
+
55
+ function ShopShell({ children }: { children: React.ReactNode }) {
56
+ return (
57
+ <div className="shop">
58
+ <NavBar />
59
+ <main>
60
+ <Outlet /> {/* fade applies HERE */}
61
+ </main>
62
+ <Footer />
63
+ </div>
64
+ );
65
+ }
66
+
67
+ export const urlpatterns = urls(({ layout, path, transition }) => [
68
+ layout(<ShopShell />, () => [
69
+ transition({ default: "page-fade" }),
70
+ path("/", ShopIndex, { name: "index" }),
71
+ path("/about", AboutPage, { name: "about" }),
72
+ path("/contact", ContactPage, { name: "contact" }),
73
+ ]),
74
+ ]);
75
+ ```
76
+
77
+ ```css
78
+ ::view-transition-old(root) {
79
+ animation: fade-out 200ms ease both;
80
+ }
81
+ ::view-transition-new(root) {
82
+ animation: fade-in 200ms ease both;
83
+ }
84
+ .page-fade {
85
+ /* class hooks per phase */
86
+ }
87
+ ```
88
+
89
+ Navigating between `/`, `/about`, and `/contact` morphs the `<Outlet />` content with the `page-fade` class. The shell (NavBar, Footer) does not morph because the wrap sits inside the shell, not around it.
90
+
91
+ ## Direction-aware transitions
92
+
93
+ `ViewTransitionClass` accepts an object form keyed by transition type. Rango tags forward navigations as `"navigation"` and back/forward popstate as `"navigation-back"`:
94
+
95
+ ```tsx
96
+ layout(<ShopShell />, () => [
97
+ transition({
98
+ default: {
99
+ navigation: "slide-left",
100
+ "navigation-back": "slide-right",
101
+ },
102
+ }),
103
+ path("/", ShopIndex, { name: "index" }),
104
+ path("/about", AboutPage, { name: "about" }),
105
+ ]);
106
+ ```
107
+
108
+ ```css
109
+ .slide-left {
110
+ animation-name: slide-from-right;
111
+ }
112
+ .slide-right {
113
+ animation-name: slide-from-left;
114
+ }
115
+ ```
116
+
117
+ > Note: `"action"` is only tagged on partial-update action/refetch paths today; ordinary `server-action-bridge` commits (`useAction` / `useActionState` revalidations) are not currently tagged. Don't rely on an `action`-keyed class to fire on every form action.
118
+
119
+ ## Wrapper form: applying transition to a group of routes
120
+
121
+ `transition(config, () => [...])` creates a transparent layout that applies the config to its children — useful when you want a transition without authoring a real layout component:
122
+
123
+ ```tsx
124
+ urls(({ path, transition }) => [
125
+ // No layout component, but every route inside gets the fade.
126
+ transition({ default: "fade" }, () => [
127
+ path("/", HomePage, { name: "home" }),
128
+ path("/about", AboutPage, { name: "about" }),
129
+ ]),
130
+ // Outside the wrapper — no transition applied.
131
+ path("/admin", AdminPage, { name: "admin" }),
132
+ ]);
133
+ ```
134
+
135
+ ## Intercept (modal) interaction
136
+
137
+ This is where the rango-specific behavior pays off. A common shape:
138
+
139
+ ```tsx
140
+ import { urls } from "@rangojs/router";
141
+ import { Outlet, ParallelOutlet } from "@rangojs/router/client";
142
+
143
+ function GalleryShell() {
144
+ return (
145
+ <>
146
+ <NavBar />
147
+ <main>
148
+ <Outlet /> {/* page transition lands here */}
149
+ </main>
150
+ <ParallelOutlet name="@modal" />{" "}
151
+ {/* modal mounts here — sibling of the VT */}
152
+ </>
153
+ );
154
+ }
155
+
156
+ export const urlpatterns = urls(
157
+ ({ layout, path, intercept, transition, loader, loading }) => [
158
+ layout(<GalleryShell />, () => [
159
+ transition({ default: "fade" }),
160
+
161
+ path("/", GalleryFeed, { name: "feed" }),
162
+ path("/photos/:id", PhotoPage, { name: "photo" }),
163
+
164
+ intercept("@modal", "photo", <PhotoModal />, () => [
165
+ loader(PhotoLoader),
166
+ loading(<PhotoModalSkeleton />),
167
+ ]),
168
+ ]),
169
+ ],
170
+ );
171
+ ```
172
+
173
+ | Action | What fires |
174
+ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
175
+ | Navigate `/` ↔ `/about` (within `GalleryShell`) | Layout transition fires; `<Outlet />` content cross-fades |
176
+ | Click `<Link to="/photos/42" />` from `/` | Soft navigation opens `<PhotoModal />` in `@modal`; **no** view transition fires on the underlying feed |
177
+ | Submit a form action inside `<PhotoModal />` | Revalidation commits without firing the layout VT; modal subtree identity is preserved (no remount, `useActionState` survives) |
178
+ | Close modal via `router.back()` | Underlying page is restored; **no** view transition fires |
179
+ | Direct URL load `/photos/42` | Renders the full `<PhotoPage />` with no modal; the layout transition applies on subsequent in-layout navs |
180
+
181
+ The "no VT on modal open" guarantee holds at any depth — if the layout that owns `@modal` is itself nested inside another transitioned layout, the outer transition is pushed past the inner layout into its default outlet content, so the modal slot ends up outside both VTs.
182
+
183
+ ## Per-route transition
184
+
185
+ Routes are leaves: their `transition()` wraps the route component itself.
186
+
187
+ ```tsx
188
+ urls(({ path, transition }) => [
189
+ path("/checkout", CheckoutPage, { name: "checkout" }, () => [
190
+ transition({ default: "fade-in" }),
191
+ ]),
192
+ ]);
193
+ ```
194
+
195
+ This is the right level for one-off route-specific morphs that should not propagate to siblings.
196
+
197
+ ## TransitionConfig
198
+
199
+ `transition()` accepts the props of React's `<ViewTransition>` (minus `children`/refs). Each phase prop accepts either a plain class string or an object keyed by transition type:
200
+
201
+ ```ts
202
+ import type { TransitionConfig } from "@rangojs/router";
203
+
204
+ interface TransitionConfig {
205
+ enter?: string | Record<string, string>;
206
+ exit?: string | Record<string, string>;
207
+ update?: string | Record<string, string>;
208
+ share?: string | Record<string, string>;
209
+ default?: string | Record<string, string>; // fallback for any phase
210
+ name?: string; // explicit view-transition-name
211
+ viewTransition?: "auto" | false; // boundary opt-out (see below)
212
+ // Conditional gate, evaluated server-side AFTER the route handler. Return
213
+ // false to drop this transition for the request, so the navigation streams its
214
+ // loading() fallback instead of holding. See the gate section below.
215
+ when?: (ctx: TransitionWhenContext) => boolean;
216
+ }
217
+ ```
218
+
219
+ - `default` is the catch-all if a phase-specific prop is unset.
220
+ - The object form keys are React transition types tagged by rango: `"navigation"` (forward navigations), `"navigation-back"` (popstate cache restores), and `"action"` (partial-update action/refetch paths only — see the caveat in "Direction-aware transitions").
221
+ - `name` lets you participate in cross-page morphs by name (advanced; you usually don't need this on a layout/route-level wrap).
222
+ - `viewTransition` toggles whether rango places its own `<ViewTransition>` boundary. `"auto"` (default) wraps as described above; `false` opts out — see the next section.
223
+
224
+ ## Conditional transitions (`when`)
225
+
226
+ `transition({ when })` gates the hold per request. The predicate runs **server-side, AFTER the route handler** and outside any cache scope; return `false` to drop this segment's transition for the request (the navigation streams its `loading()` fallback instead of holding).
227
+
228
+ Its context mirrors the `revalidate()` predicate args — the same navigation/action metadata — plus `get`/`env` for post-handler reads:
229
+
230
+ ```ts
231
+ import type { TransitionWhenContext } from "@rangojs/router";
232
+
233
+ // Hold only when the handler marked this request (handler sets, gate reads):
234
+ transition({ when: (ctx) => ctx.get(KeepScroll) === true });
235
+
236
+ // Hold only when arriving from a specific page (the navigation SOURCE):
237
+ transition({
238
+ when: ({ currentUrl }) => currentUrl?.pathname.startsWith("/list") === true,
239
+ });
240
+ transition({ when: ({ fromRouteName }) => fromRouteName === "products.list" });
241
+
242
+ // Hold only after a specific action revalidated the route:
243
+ transition({
244
+ when: ({ actionId }) => actionId === "src/actions/cart.ts#addToCart",
245
+ });
246
+ ```
247
+
248
+ | field | meaning | populated |
249
+ | ------------------------------------------------------ | -------------------------------------------- | ------------------------------------------------------------------------------------- |
250
+ | `currentUrl` / `currentParams` / `fromRouteName` | navigation **source** | soft nav + action-success; `undefined` on initial full load and action/PE error paths |
251
+ | `nextUrl` / `nextParams` | navigation **target** | always |
252
+ | `toRouteName` (and `fromRouteName`) | route **name** | when the route is named (undefined for unnamed/auto-generated) |
253
+ | `actionId` / `actionUrl` / `actionResult` / `formData` | the server action that triggered this render | action-triggered renders only |
254
+ | `method` | `"GET"` (nav) / `"POST"` (action) | always |
255
+ | `get` / `env` | read handler/middleware vars + app env | always |
256
+
257
+ A predicate that throws is reported to `router.onError` (phase `"rendering"`) and treated as no-hold (conservative).
258
+
259
+ **Same-route content-holds need the transition present on the FIRST render.** The same-route hold works by giving the route a param-agnostic key so a param change reconciles instead of remounting — but that key is established when the route first mounts. A source gate that returns `false` on the initial full load (where `currentUrl`/`currentParams`/`fromRouteName` are undefined) drops the transition before the route mounts, so the route mounts _outside_ a transition scope and **every** later same-route param nav remounts (flashing the skeleton) regardless of what the gate decides on those navs. Write source gates so they hold when there is no source — e.g. `({ currentParams }) => currentParams?.tab !== "raw"` (true on the initial load) rather than `=== "details"` (false on the initial load) — when the same-route content-hold must engage. This only affects same-route param navigations; action-only or cross-route gating is unaffected (no shared param key is in play).
260
+
261
+ **Prefetch / cache caveat.** The gate runs during resolution, so a **prefetched** navigation decides at prefetch time — `currentUrl`/`currentParams`/`fromRouteName` reflect the page the prefetch fired from, not necessarily the click-time source — and a `cache()`/prerender hit replays the stored transition without re-running the predicate. A source-sensitive gate can therefore be frozen to prefetch/store-time state. This covers ~99% of navigations; if yours must reflect the exact click-time source, source-scope the prefetch (`<Link prefetchKey=":source">`) and don't `cache()` that segment.
262
+
263
+ ## Opting out of the router boundary (place your own `<ViewTransition>`)
264
+
265
+ By default a `transition()` segment gets a rango-placed `<ViewTransition>` boundary — a cross-fade of the whole outlet/route. If you'd rather animate specific elements yourself (place `<ViewTransition name="...">` in your components), set `viewTransition: false`. The router then contributes **no boundary of its own** but still:
266
+
267
+ - drives the navigation commit through `startTransition` (so React runs `document.startViewTransition`, and your own `<ViewTransition>` elements animate on navigation — driving is what they need, not a router boundary), and
268
+ - holds same-route content (stale-while-revalidate; no skeleton flash).
269
+
270
+ ```tsx
271
+ // Router drives the transition + holds content, but places NO cross-fade.
272
+ // Only your <ViewTransition name="hero"> morphs.
273
+ urls(({ path, transition }) => [
274
+ path("/product/:id", ProductPage, { name: "product" }, () => [
275
+ transition({ viewTransition: false }),
276
+ ]),
277
+ ]);
278
+
279
+ // ProductPage renders the boundary itself, exactly where it's wanted:
280
+ function ProductPage() {
281
+ return (
282
+ <ViewTransition name="hero">
283
+ <img src={cover} />
284
+ </ViewTransition>
285
+ );
286
+ }
287
+ ```
288
+
289
+ This is the rango analogue of the "router triggers, you place the names" model used by React Router / TanStack: rango guarantees navigations run inside a React transition; you own the boundaries.
290
+
291
+ **App-wide default.** Flip the default for every `transition()` segment at the router level. A per-segment `viewTransition` still overrides it.
292
+
293
+ ```ts
294
+ const router = createRouter<AppEnv>({ viewTransition: false });
295
+ // Now `transition({})` drives + holds but places no boundary anywhere.
296
+ // Re-enable a router boundary on one route with transition({ viewTransition: "auto" }).
297
+ ```
298
+
299
+ **Precedence (per-route vs router default).** A bare `transition({})` has no per-route `viewTransition`, so it inherits the router default (`"auto"` unless `createRouter({ viewTransition: false })`). An explicit per-route value always wins. The `viewTransition` flag only toggles the boundary — `startTransition` driving and content-hold are on in every row below (they key off `transition()` presence, not this flag):
300
+
301
+ | per-route (`transition(...)`) | router (`createRouter`) | resolved boundary | result |
302
+ | ---------------------------------------- | ----------------------- | ------------------------ | ----------- |
303
+ | `transition({})` (unset) | `"auto"` (default) | wrap | **ST + VT** |
304
+ | `transition({})` (unset) | `false` | no wrap | **ST only** |
305
+ | `transition({ viewTransition: "auto" })` | `"auto"` | wrap | ST + VT |
306
+ | `transition({ viewTransition: "auto" })` | `false` | wrap (per-route wins) | **ST + VT** |
307
+ | `transition({ viewTransition: false })` | `"auto"` | no wrap (per-route wins) | **ST only** |
308
+ | `transition({ viewTransition: false })` | `false` | no wrap | ST only |
309
+
310
+ On stable React the "VT" column is always a no-op (there is no `<ViewTransition>`), so every row collapses to its `startTransition`-only behavior there.
311
+
312
+ | Config | Router boundary | startTransition driving (no skeleton flash) | Your own `<ViewTransition name>` |
313
+ | ---------------------------------------------------- | ---------------- | ------------------------------------------- | ---------------------------------- |
314
+ | no `transition()` | — | no | does not fire on nav |
315
+ | `transition({})` / `{ viewTransition: "auto" }` | yes (cross-fade) | yes | fires, under the router cross-fade |
316
+ | `transition({ viewTransition: false })` | none | yes | fires alone |
317
+ | global `viewTransition: false`, route `transition()` | none | yes | fires alone |
318
+
319
+ > On **stable** React there is no `<ViewTransition>` at all, so `viewTransition: false` is visually a no-op there — but the startTransition driving and content-hold still apply, identical to `transition({})`.
320
+
321
+ ## Recommendations
322
+
323
+ **Put `<ParallelOutlet />` in layouts, not routes.** A route-level `transition` wraps the route component itself, so a `<ParallelOutlet />` rendered directly inside that route component remains inside the route VT subtree — modal opens on a route with a parallel outlet _will_ trigger the route's VT walker. The narrowing fix only applies at layout boundaries. If you combine intercept modals with route-level transitions, mount the slot one level up in a layout.
324
+
325
+ **Don't stack `transition()` on every layout level.** When ancestor and descendant layouts both configure transitions, both wraps end up nested around the deepest default outlet content. Two VTs fire on every nav within the inner layout. That's usually not what you want — pick the level where the morph belongs and apply it once.
326
+
327
+ **Need a modal-only morph?** Per-slot `transition()` is currently a no-op at render time, so use an element-level React `<ViewTransition>` inside the modal component (or a CSS animation) for the modal-entrance effect.
328
+
329
+ **Action revalidation inside a modal is safe.** Server-action submits inside an open modal don't fire the underlying layout VT. Modal subtree identity is preserved across revalidation — so `useActionState`, focus, and scroll all survive the round-trip.
330
+
331
+ ## Notes
332
+
333
+ - `transition()` is part of the route DSL. The allow-list table in [skills/handler-use](../handler-use/SKILL.md) permits it inside `layout()`, `path()`/`route()`, `parallel()` (per-slot or shared), and `intercept()`. At render time, only the layout and route wraps actually take effect today; `parallel()`/`intercept()` slot-level rendering does not currently apply the wrap.
334
+ - Wrap location for layouts: rango walks the rendered tree past `MountContextProvider`/`OutletProvider`/`LoaderBoundary` for layout segments and applies the wrap at the first non-layout target ([segment-system.tsx](../../src/segment-system.tsx) — `wrapDefaultOutletContent`). This is what keeps parallel slots out of the VT subtree.
335
+ - Tree consistency: the wrapper structure is identical across normal commits, intercept-active commits, and action revalidations — React never sees an element-type swap, so layout/modal subtrees are not remounted across these transitions.
336
+ - Element-level `<ViewTransition>` (importing it directly from React and using `name`/`share` to morph specific elements across pages) composes with rango's segment-level wraps as usual; rango doesn't intercept those.
337
+ - See also: [skills/intercept](../intercept/SKILL.md), [skills/parallel](../parallel/SKILL.md), [skills/layout](../layout/SKILL.md).
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Simulates a consumer augmenting the Rango global namespace, the way a real
3
+ * app does in router.tsx (Env/Vars) and via the generated router.named-routes.gen.ts
4
+ * (GeneratedRouteMap).
5
+ *
6
+ * This file is compiled ONLY by tsconfig.augment-check.json and is excluded from
7
+ * the main program, so the global augmentation here does not leak into the rest
8
+ * of the type tests, which assert the UNAUGMENTED fallbacks
9
+ * (see src/__tests__/augmentation-fallback-types.test.ts).
10
+ */
11
+
12
+ export interface TestBindings {
13
+ DB: { query: (sql: string) => string };
14
+ SECRET: string;
15
+ }
16
+
17
+ export interface TestVars {
18
+ user?: { id: string; role: "admin" | "user" };
19
+ requestId?: string;
20
+ }
21
+
22
+ /**
23
+ * A userland domain type that controls its own JSON wire shape via `toJSON()`.
24
+ * This is the augmentation hook: `Rango.JsonSerialize` honors `toJSON()`, so a
25
+ * consumer adjusts how their type serializes with no registry API.
26
+ */
27
+ export class Money {
28
+ constructor(public cents: number) {}
29
+ toJSON(): number {
30
+ return this.cents;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Mirrors `typeof router.routeMap`: the same routes as the generated map, plus a
36
+ * response route whose payload carries a userland class (`Money`, with
37
+ * `toJSON()`) and a `Date` — used to assert `Rango.PathResponse` reports the
38
+ * serialized JSON wire shape.
39
+ */
40
+ export interface TestRegisteredRoutes {
41
+ readonly home: "/";
42
+ readonly "blog.post": "/blog/:slug";
43
+ readonly search: {
44
+ readonly path: "/search";
45
+ readonly search: { readonly q: "string"; readonly page: "number?" };
46
+ };
47
+ readonly order: {
48
+ readonly path: "/orders/:id";
49
+ readonly response: { id: string; total: Money; placedAt: Date };
50
+ };
51
+ }
52
+
53
+ declare global {
54
+ namespace Rango {
55
+ interface Env extends TestBindings {}
56
+ interface Vars extends TestVars {}
57
+ interface RegisteredRoutes extends TestRegisteredRoutes {}
58
+ // Mirrors the shape emitted into router.named-routes.gen.ts: plain string
59
+ // patterns, plus { path, search } objects for routes with a search schema.
60
+ interface GeneratedRouteMap {
61
+ readonly home: "/";
62
+ readonly "blog.post": "/blog/:slug";
63
+ readonly search: {
64
+ readonly path: "/search";
65
+ readonly search: { readonly q: "string"; readonly page: "number?" };
66
+ };
67
+ }
68
+
69
+ // Userland serialization overrides: full-transform replacement that
70
+ // special-cases one type and delegates the rest to the built-in. Isolated to
71
+ // this augment-check program, so the main suite's built-in behavior (e.g.
72
+ // JsonSerialize<bigint> = never) is unaffected — demonstrating overrides are
73
+ // per-project augmentation.
74
+ interface JsonSerializeOverride<T> {
75
+ app: T extends bigint ? string : Rango.JsonSerializeBuiltin<T>;
76
+ }
77
+ interface FlightSerializeOverride<T> {
78
+ app: T extends Money ? number : Rango.FlightSerializeBuiltin<T>;
79
+ }
80
+ }
81
+ }