@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bdaf10aa

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 (449) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +303 -741
  3. package/dist/bin/rango.js +730 -184
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +4344 -1335
  6. package/dist/vite/index.js.bak +5448 -0
  7. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  8. package/package.json +86 -15
  9. package/skills/api-client/SKILL.md +211 -0
  10. package/skills/breadcrumbs/SKILL.md +85 -6
  11. package/skills/bundle-analysis/SKILL.md +159 -0
  12. package/skills/cache-guide/SKILL.md +251 -24
  13. package/skills/caching/SKILL.md +375 -17
  14. package/skills/catalog.json +271 -0
  15. package/skills/comparison/SKILL.md +50 -0
  16. package/skills/comparison/agents/openai.yaml +4 -0
  17. package/skills/comparison/references/framework-comparison.md +837 -0
  18. package/skills/composability/SKILL.md +110 -4
  19. package/skills/css/SKILL.md +76 -0
  20. package/skills/debug-manifest/SKILL.md +5 -3
  21. package/skills/defer-hydration/SKILL.md +235 -0
  22. package/skills/document-cache/SKILL.md +87 -56
  23. package/skills/fonts/SKILL.md +1 -1
  24. package/skills/handler-use/SKILL.md +364 -0
  25. package/skills/hooks/SKILL.md +73 -691
  26. package/skills/hooks/data.md +273 -0
  27. package/skills/hooks/handle-and-actions.md +103 -0
  28. package/skills/hooks/navigation.md +110 -0
  29. package/skills/hooks/outlets.md +41 -0
  30. package/skills/hooks/state.md +228 -0
  31. package/skills/hooks/urls.md +135 -0
  32. package/skills/host-router/SKILL.md +129 -27
  33. package/skills/i18n/SKILL.md +276 -0
  34. package/skills/intercept/SKILL.md +94 -18
  35. package/skills/layout/SKILL.md +62 -19
  36. package/skills/links/SKILL.md +249 -17
  37. package/skills/loader/SKILL.md +302 -54
  38. package/skills/middleware/SKILL.md +59 -16
  39. package/skills/migrate-nextjs/SKILL.md +745 -0
  40. package/skills/migrate-react-router/SKILL.md +153 -0
  41. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  42. package/skills/migrate-react-router/component-migration.md +196 -0
  43. package/skills/migrate-react-router/data-and-actions.md +225 -0
  44. package/skills/migrate-react-router/route-mapping.md +271 -0
  45. package/skills/mime-routes/SKILL.md +29 -2
  46. package/skills/observability/SKILL.md +202 -0
  47. package/skills/parallel/SKILL.md +225 -10
  48. package/skills/ppr/SKILL.md +622 -0
  49. package/skills/prerender/SKILL.md +178 -124
  50. package/skills/rango/SKILL.md +318 -24
  51. package/skills/react-compiler/SKILL.md +168 -0
  52. package/skills/response-routes/SKILL.md +138 -49
  53. package/skills/route/SKILL.md +172 -9
  54. package/skills/router-setup/SKILL.md +131 -11
  55. package/skills/scripts/SKILL.md +179 -0
  56. package/skills/server-actions/SKILL.md +776 -0
  57. package/skills/shell-manifest/SKILL.md +185 -0
  58. package/skills/streams-and-websockets/SKILL.md +283 -0
  59. package/skills/tailwind/SKILL.md +28 -4
  60. package/skills/testing/SKILL.md +130 -0
  61. package/skills/testing/bindings.md +103 -0
  62. package/skills/testing/cache-prerender.md +127 -0
  63. package/skills/testing/client-components.md +124 -0
  64. package/skills/testing/e2e-parity.md +125 -0
  65. package/skills/testing/flight.md +91 -0
  66. package/skills/testing/handles.md +131 -0
  67. package/skills/testing/loader.md +128 -0
  68. package/skills/testing/middleware.md +99 -0
  69. package/skills/testing/render-handler.md +122 -0
  70. package/skills/testing/response-routes.md +95 -0
  71. package/skills/testing/reverse-and-types.md +85 -0
  72. package/skills/testing/server-actions.md +107 -0
  73. package/skills/testing/server-tree.md +128 -0
  74. package/skills/testing/setup.md +123 -0
  75. package/skills/theme/SKILL.md +1 -1
  76. package/skills/typesafety/SKILL.md +45 -616
  77. package/skills/typesafety/env-and-bindings.md +254 -0
  78. package/skills/typesafety/generated-files-and-cli.md +335 -0
  79. package/skills/typesafety/params-and-search.md +153 -0
  80. package/skills/typesafety/route-types.md +209 -0
  81. package/skills/use-cache/SKILL.md +74 -15
  82. package/skills/vercel/SKILL.md +128 -0
  83. package/skills/view-transitions/SKILL.md +337 -0
  84. package/src/__augment-tests__/augment.ts +81 -0
  85. package/src/__augment-tests__/augmented.check.ts +116 -0
  86. package/src/__internal.ts +1 -66
  87. package/src/browser/action-coordinator.ts +53 -36
  88. package/src/browser/action-fence.ts +47 -0
  89. package/src/browser/app-shell.ts +39 -0
  90. package/src/browser/app-version.ts +14 -0
  91. package/src/browser/connection-warmup.ts +134 -0
  92. package/src/browser/cookie-name.ts +140 -0
  93. package/src/browser/event-controller.ts +257 -158
  94. package/src/browser/history-state.ts +21 -0
  95. package/src/browser/index.ts +3 -3
  96. package/src/browser/invalidate-client-cache.ts +52 -0
  97. package/src/browser/logging.ts +28 -0
  98. package/src/browser/merge-segment-loaders.ts +6 -4
  99. package/src/browser/navigation-bridge.ts +132 -33
  100. package/src/browser/navigation-client.ts +218 -68
  101. package/src/browser/navigation-store-handle.ts +38 -0
  102. package/src/browser/navigation-store.ts +203 -80
  103. package/src/browser/navigation-transaction.ts +18 -66
  104. package/src/browser/network-error-handler.ts +34 -7
  105. package/src/browser/partial-update.ts +241 -127
  106. package/src/browser/prefetch/cache.ts +271 -44
  107. package/src/browser/prefetch/fetch.ts +367 -40
  108. package/src/browser/prefetch/queue.ts +144 -23
  109. package/src/browser/prefetch/resource-ready.ts +77 -0
  110. package/src/browser/rango-state.ts +158 -76
  111. package/src/browser/react/Link.tsx +121 -16
  112. package/src/browser/react/NavigationProvider.tsx +240 -122
  113. package/src/browser/react/ScrollRestoration.tsx +10 -6
  114. package/src/browser/react/context.ts +7 -2
  115. package/src/browser/react/filter-segment-order.ts +66 -7
  116. package/src/browser/react/index.ts +0 -48
  117. package/src/browser/react/location-state-shared.ts +178 -8
  118. package/src/browser/react/location-state.ts +39 -14
  119. package/src/browser/react/use-action.ts +6 -15
  120. package/src/browser/react/use-handle.ts +23 -69
  121. package/src/browser/react/use-href.tsx +8 -1
  122. package/src/browser/react/use-link-status.ts +33 -8
  123. package/src/browser/react/use-navigation.ts +32 -7
  124. package/src/browser/react/use-params.ts +20 -10
  125. package/src/browser/react/use-reverse.ts +106 -0
  126. package/src/browser/react/use-router.ts +46 -11
  127. package/src/browser/react/use-search-params.ts +0 -5
  128. package/src/browser/react/use-segments.ts +11 -21
  129. package/src/browser/response-adapter.ts +99 -8
  130. package/src/browser/rsc-router.tsx +272 -80
  131. package/src/browser/scroll-restoration.ts +56 -22
  132. package/src/browser/segment-reconciler.ts +44 -7
  133. package/src/browser/segment-structure-assert.ts +2 -2
  134. package/src/browser/server-action-bridge.ts +244 -71
  135. package/src/browser/types.ts +136 -12
  136. package/src/browser/validate-redirect-origin.ts +43 -16
  137. package/src/build/collect-fallback-refs.ts +107 -0
  138. package/src/build/generate-manifest.ts +207 -158
  139. package/src/build/generate-route-types.ts +6 -1
  140. package/src/build/index.ts +11 -3
  141. package/src/build/prefix-tree-utils.ts +123 -0
  142. package/src/build/route-trie.ts +198 -41
  143. package/src/build/route-types/ast-route-extraction.ts +15 -8
  144. package/src/build/route-types/codegen.ts +16 -5
  145. package/src/build/route-types/include-resolution.ts +464 -63
  146. package/src/build/route-types/param-extraction.ts +6 -3
  147. package/src/build/route-types/per-module-writer.ts +22 -6
  148. package/src/build/route-types/router-processing.ts +336 -110
  149. package/src/build/route-types/scan-filter.ts +9 -2
  150. package/src/build/route-types/source-scan.ts +216 -0
  151. package/src/build/runtime-discovery.ts +13 -21
  152. package/src/cache/cache-error.ts +104 -0
  153. package/src/cache/cache-key-utils.ts +58 -13
  154. package/src/cache/cache-policy.ts +108 -34
  155. package/src/cache/cache-runtime.ts +454 -97
  156. package/src/cache/cache-scope.ts +235 -103
  157. package/src/cache/cache-tag.ts +149 -0
  158. package/src/cache/cf/cf-base64.ts +33 -0
  159. package/src/cache/cf/cf-cache-constants.ts +127 -0
  160. package/src/cache/cf/cf-cache-store.ts +2446 -170
  161. package/src/cache/cf/cf-cache-types.ts +349 -0
  162. package/src/cache/cf/cf-kv-utils.ts +46 -0
  163. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  164. package/src/cache/cf/index.ts +11 -17
  165. package/src/cache/document-cache.ts +144 -49
  166. package/src/cache/handle-snapshot.ts +70 -0
  167. package/src/cache/index.ts +24 -20
  168. package/src/cache/memory-segment-store.ts +243 -37
  169. package/src/cache/profile-registry.ts +46 -31
  170. package/src/cache/read-through-swr.ts +56 -12
  171. package/src/cache/segment-codec.ts +13 -21
  172. package/src/cache/shell-snapshot.ts +417 -0
  173. package/src/cache/tag-invalidation.ts +230 -0
  174. package/src/cache/taint.ts +55 -0
  175. package/src/cache/types.ts +194 -99
  176. package/src/cache/vercel/index.ts +11 -0
  177. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  178. package/src/client.rsc.tsx +41 -21
  179. package/src/client.tsx +116 -290
  180. package/src/cloudflare/index.ts +11 -0
  181. package/src/cloudflare/tracing.ts +108 -0
  182. package/src/component-utils.ts +19 -0
  183. package/src/components/DefaultDocument.tsx +8 -2
  184. package/src/context-var.ts +84 -2
  185. package/src/debug.ts +2 -2
  186. package/src/decode-loader-results.ts +52 -0
  187. package/src/defer.ts +185 -0
  188. package/src/deps/ssr.ts +0 -1
  189. package/src/encode-kv.ts +49 -0
  190. package/src/errors.ts +30 -4
  191. package/src/escape-script.ts +52 -0
  192. package/src/handle.ts +104 -34
  193. package/src/handles/MetaTags.tsx +24 -53
  194. package/src/handles/Scripts.tsx +183 -0
  195. package/src/handles/breadcrumbs.ts +35 -8
  196. package/src/handles/deferred-resolution.ts +127 -0
  197. package/src/handles/is-thenable.ts +18 -0
  198. package/src/handles/meta.ts +14 -40
  199. package/src/handles/script.ts +244 -0
  200. package/src/host/cookie-handler.ts +9 -60
  201. package/src/host/errors.ts +13 -22
  202. package/src/host/index.ts +9 -2
  203. package/src/host/pattern-matcher.ts +23 -52
  204. package/src/host/router.ts +107 -99
  205. package/src/host/testing.ts +40 -27
  206. package/src/host/types.ts +37 -4
  207. package/src/host/utils.ts +1 -1
  208. package/src/href-client.ts +137 -22
  209. package/src/index.rsc.ts +100 -13
  210. package/src/index.ts +143 -19
  211. package/src/internal-debug.ts +11 -10
  212. package/src/loader-store.ts +500 -0
  213. package/src/loader.rsc.ts +20 -13
  214. package/src/loader.ts +12 -11
  215. package/src/missing-id-error.ts +68 -0
  216. package/src/outlet-context.ts +1 -1
  217. package/src/outlet-provider.tsx +1 -5
  218. package/src/prerender/param-hash.ts +16 -16
  219. package/src/prerender/store.ts +37 -41
  220. package/src/prerender.ts +215 -86
  221. package/src/redirect-origin.ts +114 -0
  222. package/src/regex-escape.ts +8 -0
  223. package/src/render-error-thrower.tsx +20 -0
  224. package/src/response-utils.ts +62 -0
  225. package/src/reverse.ts +65 -15
  226. package/src/root-error-boundary.tsx +1 -19
  227. package/src/route-content-wrapper.tsx +19 -77
  228. package/src/route-definition/dsl-helpers.ts +485 -303
  229. package/src/route-definition/helper-factories.ts +28 -140
  230. package/src/route-definition/helpers-types.ts +153 -77
  231. package/src/route-definition/index.ts +4 -2
  232. package/src/route-definition/redirect.ts +53 -12
  233. package/src/route-definition/resolve-handler-use.ts +160 -0
  234. package/src/route-definition/use-item-types.ts +29 -0
  235. package/src/route-map-builder.ts +48 -21
  236. package/src/route-types.ts +37 -46
  237. package/src/router/basename.ts +14 -0
  238. package/src/router/content-negotiation.ts +164 -17
  239. package/src/router/error-handling.ts +45 -18
  240. package/src/router/find-match.ts +130 -29
  241. package/src/router/handler-context.ts +83 -39
  242. package/src/router/instrument.ts +355 -0
  243. package/src/router/intercept-resolution.ts +50 -24
  244. package/src/router/lazy-includes.ts +89 -63
  245. package/src/router/loader-resolution.ts +286 -56
  246. package/src/router/logging.ts +5 -8
  247. package/src/router/manifest.ts +105 -56
  248. package/src/router/match-api.ts +178 -218
  249. package/src/router/match-context.ts +0 -22
  250. package/src/router/match-handlers.ts +211 -165
  251. package/src/router/match-middleware/background-revalidation.ts +66 -22
  252. package/src/router/match-middleware/cache-lookup.ts +214 -263
  253. package/src/router/match-middleware/cache-store.ts +105 -50
  254. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  255. package/src/router/match-middleware/segment-resolution.ts +52 -18
  256. package/src/router/match-pipelines.ts +1 -42
  257. package/src/router/match-result.ts +128 -44
  258. package/src/router/metrics.ts +5 -34
  259. package/src/router/middleware-types.ts +13 -142
  260. package/src/router/middleware.ts +301 -177
  261. package/src/router/navigation-snapshot.ts +133 -0
  262. package/src/router/params-util.ts +23 -0
  263. package/src/router/parse-pattern.ts +115 -0
  264. package/src/router/pattern-matching.ts +181 -150
  265. package/src/router/prefetch-cache-ttl.ts +51 -0
  266. package/src/router/prefetch-limits.ts +37 -0
  267. package/src/router/prerender-match.ts +203 -58
  268. package/src/router/preview-match.ts +35 -103
  269. package/src/router/request-classification.ts +291 -0
  270. package/src/router/revalidation.ts +123 -73
  271. package/src/router/route-snapshot.ts +256 -0
  272. package/src/router/router-context.ts +11 -29
  273. package/src/router/router-interfaces.ts +146 -35
  274. package/src/router/router-options.ts +202 -15
  275. package/src/router/router-registry.ts +2 -5
  276. package/src/router/segment-resolution/fresh.ts +301 -78
  277. package/src/router/segment-resolution/helpers.ts +115 -30
  278. package/src/router/segment-resolution/loader-cache.ts +156 -39
  279. package/src/router/segment-resolution/loader-mask.ts +60 -0
  280. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  281. package/src/router/segment-resolution/mask-nested.ts +83 -0
  282. package/src/router/segment-resolution/revalidation.ts +477 -385
  283. package/src/router/segment-resolution/static-store.ts +19 -5
  284. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  285. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  286. package/src/router/segment-resolution.ts +5 -1
  287. package/src/router/segment-wrappers.ts +8 -5
  288. package/src/router/state-cookie-name.ts +33 -0
  289. package/src/router/substitute-pattern-params.ts +75 -0
  290. package/src/router/telemetry-otel.ts +160 -200
  291. package/src/router/telemetry.ts +105 -20
  292. package/src/router/timeout.ts +0 -20
  293. package/src/router/tracing.ts +215 -0
  294. package/src/router/trie-matching.ts +171 -59
  295. package/src/router/types.ts +10 -63
  296. package/src/router/url-params.ts +57 -0
  297. package/src/router.ts +210 -71
  298. package/src/rsc/full-payload.ts +70 -0
  299. package/src/rsc/handler-context.ts +3 -2
  300. package/src/rsc/handler.ts +682 -508
  301. package/src/rsc/helpers.ts +168 -46
  302. package/src/rsc/index.ts +2 -5
  303. package/src/rsc/json-route-result.ts +38 -0
  304. package/src/rsc/loader-fetch.ts +127 -31
  305. package/src/rsc/manifest-init.ts +33 -42
  306. package/src/rsc/nonce.ts +10 -1
  307. package/src/rsc/origin-guard.ts +39 -25
  308. package/src/rsc/progressive-enhancement.ts +138 -15
  309. package/src/rsc/redirect-guard.ts +100 -0
  310. package/src/rsc/response-cache-serve.ts +238 -0
  311. package/src/rsc/response-error.ts +79 -12
  312. package/src/rsc/response-route-handler.ts +99 -189
  313. package/src/rsc/rsc-rendering.ts +509 -73
  314. package/src/rsc/runtime-warnings.ts +23 -10
  315. package/src/rsc/server-action.ts +287 -113
  316. package/src/rsc/shell-capture.ts +1190 -0
  317. package/src/rsc/shell-serve.ts +181 -0
  318. package/src/rsc/ssr-setup.ts +18 -2
  319. package/src/rsc/transition-gate.ts +89 -0
  320. package/src/rsc/types.ts +62 -6
  321. package/src/runtime-env.ts +18 -0
  322. package/src/search-params.ts +35 -30
  323. package/src/segment-content-promise.ts +67 -0
  324. package/src/segment-loader-promise.ts +167 -0
  325. package/src/segment-system.tsx +449 -132
  326. package/src/serialize.ts +243 -0
  327. package/src/server/context.ts +367 -61
  328. package/src/server/cookie-parse.ts +32 -0
  329. package/src/server/cookie-store.ts +152 -5
  330. package/src/server/handle-store.ts +40 -38
  331. package/src/server/loader-registry.ts +38 -46
  332. package/src/server/request-context.ts +558 -173
  333. package/src/ssr/index.tsx +491 -174
  334. package/src/ssr/inject-rsc-eager.ts +167 -0
  335. package/src/ssr/ssr-root.tsx +228 -0
  336. package/src/static-handler.ts +27 -18
  337. package/src/testing/cache-status.ts +162 -0
  338. package/src/testing/collect-handle.ts +46 -0
  339. package/src/testing/dispatch.ts +813 -0
  340. package/src/testing/dom.entry.ts +22 -0
  341. package/src/testing/e2e/fixture.ts +188 -0
  342. package/src/testing/e2e/index.ts +128 -0
  343. package/src/testing/e2e/matchers.ts +35 -0
  344. package/src/testing/e2e/page-helpers.ts +272 -0
  345. package/src/testing/e2e/parity.ts +387 -0
  346. package/src/testing/e2e/server.ts +195 -0
  347. package/src/testing/flight-matchers.ts +97 -0
  348. package/src/testing/flight-normalize.ts +11 -0
  349. package/src/testing/flight-runtime.d.ts +57 -0
  350. package/src/testing/flight-tree.ts +682 -0
  351. package/src/testing/flight.entry.ts +52 -0
  352. package/src/testing/flight.ts +257 -0
  353. package/src/testing/generated-routes.ts +199 -0
  354. package/src/testing/index.ts +105 -0
  355. package/src/testing/internal/context.ts +371 -0
  356. package/src/testing/internal/flight-client-globals.ts +30 -0
  357. package/src/testing/internal/seed-vars.ts +54 -0
  358. package/src/testing/render-handler.ts +357 -0
  359. package/src/testing/render-route.tsx +584 -0
  360. package/src/testing/run-loader.ts +385 -0
  361. package/src/testing/run-middleware.ts +205 -0
  362. package/src/testing/run-transition-when.ts +164 -0
  363. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  364. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  365. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  366. package/src/testing/vitest-stubs/version.ts +5 -0
  367. package/src/testing/vitest.ts +305 -0
  368. package/src/theme/ThemeProvider.tsx +56 -84
  369. package/src/theme/ThemeScript.tsx +7 -9
  370. package/src/theme/constants.ts +52 -13
  371. package/src/theme/index.ts +0 -7
  372. package/src/theme/theme-context.ts +1 -5
  373. package/src/theme/theme-script.ts +22 -21
  374. package/src/theme/use-theme.ts +0 -3
  375. package/src/types/boundaries.ts +0 -35
  376. package/src/types/cache-types.ts +17 -8
  377. package/src/types/error-types.ts +30 -90
  378. package/src/types/global-namespace.ts +54 -41
  379. package/src/types/handler-context.ts +234 -82
  380. package/src/types/index.ts +3 -10
  381. package/src/types/loader-types.ts +44 -15
  382. package/src/types/request-scope.ts +112 -0
  383. package/src/types/route-config.ts +20 -52
  384. package/src/types/route-entry.ts +19 -7
  385. package/src/types/segments.ts +137 -14
  386. package/src/urls/include-helper.ts +40 -75
  387. package/src/urls/include-provider.ts +71 -0
  388. package/src/urls/index.ts +2 -11
  389. package/src/urls/path-helper-types.ts +102 -23
  390. package/src/urls/path-helper.ts +62 -111
  391. package/src/urls/pattern-types.ts +84 -19
  392. package/src/urls/response-types.ts +25 -22
  393. package/src/urls/type-extraction.ts +98 -154
  394. package/src/urls/urls-function.ts +1 -19
  395. package/src/use-loader.tsx +346 -89
  396. package/src/vercel/index.ts +11 -0
  397. package/src/vercel/tracing.ts +88 -0
  398. package/src/vite/debug.ts +185 -0
  399. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  400. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  401. package/src/vite/discovery/discover-routers.ts +130 -85
  402. package/src/vite/discovery/discovery-errors.ts +255 -0
  403. package/src/vite/discovery/gate-state.ts +171 -0
  404. package/src/vite/discovery/prerender-collection.ts +214 -132
  405. package/src/vite/discovery/route-types-writer.ts +40 -84
  406. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  407. package/src/vite/discovery/state.ts +57 -6
  408. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  409. package/src/vite/index.ts +15 -0
  410. package/src/vite/inject-client-debug.ts +88 -0
  411. package/src/vite/plugin-types.ts +234 -62
  412. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  413. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  414. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  415. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  416. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  417. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  418. package/src/vite/plugins/expose-action-id.ts +49 -98
  419. package/src/vite/plugins/expose-id-utils.ts +96 -51
  420. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  421. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  422. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  423. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  424. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  425. package/src/vite/plugins/performance-tracks.ts +89 -0
  426. package/src/vite/plugins/refresh-cmd.ts +89 -27
  427. package/src/vite/plugins/use-cache-transform.ts +73 -83
  428. package/src/vite/plugins/vercel-output.ts +384 -0
  429. package/src/vite/plugins/version-injector.ts +40 -29
  430. package/src/vite/plugins/version-plugin.ts +46 -37
  431. package/src/vite/plugins/virtual-entries.ts +138 -27
  432. package/src/vite/rango.ts +353 -303
  433. package/src/vite/router-discovery.ts +1090 -166
  434. package/src/vite/utils/ast-handler-extract.ts +26 -35
  435. package/src/vite/utils/banner.ts +4 -4
  436. package/src/vite/utils/bundle-analysis.ts +10 -15
  437. package/src/vite/utils/client-chunks.ts +184 -0
  438. package/src/vite/utils/directive-prologue.ts +40 -0
  439. package/src/vite/utils/forward-user-plugins.ts +171 -0
  440. package/src/vite/utils/manifest-utils.ts +4 -59
  441. package/src/vite/utils/package-resolution.ts +20 -52
  442. package/src/vite/utils/prerender-utils.ts +98 -38
  443. package/src/vite/utils/shared-utils.ts +144 -44
  444. package/src/browser/action-response-classifier.ts +0 -99
  445. package/src/browser/react/use-client-cache.ts +0 -58
  446. package/src/browser/shallow.ts +0 -40
  447. package/src/handles/index.ts +0 -7
  448. package/src/network-error-thrower.tsx +0 -23
  449. package/src/router/middleware-cookies.ts +0 -55
@@ -14,11 +14,38 @@ const addTransitionType: ((type: string) => void) | undefined =
14
14
  import type { RenderSegmentsOptions } from "../segment-system.js";
15
15
  import { reconcileSegments } from "./segment-reconciler.js";
16
16
  import type { ReconcileActor } from "./segment-reconciler.js";
17
- import { hasActiveIntercept as hasActiveInterceptSlots } from "./intercept-utils.js";
17
+ import {
18
+ hasActiveIntercept as hasActiveInterceptSlots,
19
+ isInterceptSegment,
20
+ } from "./intercept-utils.js";
18
21
  import type { BoundTransaction } from "./navigation-transaction.js";
19
22
  import { ServerRedirect } from "../errors.js";
20
- import { debugLog } from "./logging.js";
21
- import { validateRedirectOrigin } from "./validate-redirect-origin.js";
23
+ import {
24
+ debugLog,
25
+ isBrowserDebugEnabled,
26
+ IS_BROWSER_DEBUG,
27
+ } from "./logging.js";
28
+ import {
29
+ validateRedirectOrigin,
30
+ validateExternalRedirect,
31
+ } from "./validate-redirect-origin.js";
32
+ import type { NavigationUpdate } from "./types.js";
33
+
34
+ function toScrollPayload(
35
+ scroll: boolean | undefined,
36
+ ): NonNullable<NavigationUpdate["scroll"]> {
37
+ return { enabled: scroll !== false ? scroll : false };
38
+ }
39
+
40
+ function shouldStartViewTransition(segments: ResolvedSegment[]): boolean {
41
+ let hasIntercept = false;
42
+ let hasTransition = false;
43
+ for (const s of segments) {
44
+ if (isInterceptSegment(s)) hasIntercept = true;
45
+ else if (s.transition) hasTransition = true;
46
+ }
47
+ return !hasIntercept && hasTransition;
48
+ }
22
49
 
23
50
  /**
24
51
  * Configuration for creating a partial updater
@@ -31,8 +58,8 @@ export interface PartialUpdateConfig {
31
58
  segments: ResolvedSegment[],
32
59
  options?: RenderSegmentsOptions,
33
60
  ) => Promise<ReactNode> | ReactNode;
34
- /** RSC version received from server (from initial payload metadata) */
35
- version?: string;
61
+ /** RSC version getter returns the current version (may change after HMR) */
62
+ getVersion?: () => string | undefined;
36
63
  }
37
64
 
38
65
  /**
@@ -68,7 +95,7 @@ export type UpdateMode =
68
95
  /** Source URL for intercept restore (popstate cache miss) */
69
96
  interceptSourceUrl?: string;
70
97
  }
71
- | { type: "leave-intercept" }
98
+ | { type: "leave-intercept"; interceptSourceUrl?: string }
72
99
  | { type: "stale-revalidation"; interceptSourceUrl?: string }
73
100
  | { type: "action"; interceptSourceUrl?: string };
74
101
 
@@ -84,35 +111,23 @@ export type PartialUpdater = (
84
111
  mode?: UpdateMode,
85
112
  ) => Promise<void>;
86
113
 
87
- /**
88
- * Create a partial updater for fetching and applying RSC partial updates
89
- *
90
- * This function is shared between navigation-bridge and server-action-bridge
91
- * to handle partial RSC updates with HMR resilience.
92
- *
93
- * @param config - Partial update configuration
94
- * @returns fetchPartialUpdate function
95
- */
96
114
  export function createPartialUpdater(
97
115
  config: PartialUpdateConfig,
98
116
  ): PartialUpdater {
99
- const { store, client, onUpdate, renderSegments, version } = config;
117
+ const {
118
+ store,
119
+ client,
120
+ onUpdate,
121
+ renderSegments,
122
+ getVersion = () => undefined,
123
+ } = config;
100
124
 
101
- /**
102
- * Get current page's cached segments as an array
103
- */
104
125
  function getCurrentCachedSegments(): ResolvedSegment[] {
105
126
  const currentKey = store.getHistoryKey();
106
127
  const cached = store.getCachedSegments(currentKey);
107
128
  return cached?.segments || [];
108
129
  }
109
130
 
110
- /**
111
- * Fetch partial update and trigger UI update
112
- *
113
- * @param tx - Transaction for committing segment state (required)
114
- * @param signal - AbortSignal to check if navigation is stale (not for aborting fetch)
115
- */
116
131
  async function fetchPartialUpdate(
117
132
  targetUrl: string,
118
133
  segmentIds: string[] | undefined,
@@ -124,86 +139,114 @@ export function createPartialUpdater(
124
139
  const segmentState = store.getSegmentState();
125
140
  const url = targetUrl || window.location.href;
126
141
 
127
- // Capture history key at start for stale revalidation consistency check
128
142
  const historyKeyAtStart = store.getHistoryKey();
129
143
 
130
- // Derive interceptSourceUrl from modes that carry it
131
- const interceptSourceUrl =
132
- mode.type === "stale-revalidation" ||
133
- mode.type === "action" ||
134
- mode.type === "navigate"
135
- ? mode.interceptSourceUrl
136
- : undefined;
144
+ const interceptSourceUrl = mode.interceptSourceUrl;
137
145
 
138
- // When leaving intercept, filter out intercept-specific segments
139
146
  let segments: string[];
140
147
  if (mode.type === "leave-intercept") {
141
148
  const currentSegments = segmentIds ?? segmentState.currentSegmentIds;
142
149
  const currentCached = getCurrentCachedSegments();
143
150
  const interceptIds = new Set(
144
- currentCached
145
- .filter((s) => s.namespace?.startsWith("intercept:"))
146
- .map((s) => s.id),
151
+ currentCached.filter(isInterceptSegment).map((s) => s.id),
147
152
  );
148
153
  segments = currentSegments.filter((id) => !interceptIds.has(id));
149
- debugLog(
150
- `[Browser] Leaving intercept - filtered segments: ${segments.join(", ")}`,
151
- );
154
+ if (IS_BROWSER_DEBUG) {
155
+ debugLog(
156
+ `[Browser] Leaving intercept - filtered segments: ${segments.join(", ")}`,
157
+ );
158
+ }
152
159
  } else {
153
160
  segments = segmentIds ?? segmentState.currentSegmentIds;
154
161
  }
155
162
 
156
- // For intercept revalidation, use the intercept source URL as previousUrl
157
163
  const previousUrl =
158
- interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
159
-
160
- debugLog(`\n[Browser] >>> NAVIGATION`);
161
- debugLog(`[Browser] From: ${previousUrl}`);
162
- debugLog(`[Browser] To: ${url}`);
163
- debugLog(`[Browser] Segments to send: ${segments.join(", ")}`);
164
- if (interceptSourceUrl) {
165
- debugLog(`[Browser] Intercept context from: ${interceptSourceUrl}`);
164
+ mode.type === "leave-intercept"
165
+ ? segmentState.currentUrl || tx.currentUrl
166
+ : interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
167
+
168
+ if (IS_BROWSER_DEBUG) {
169
+ debugLog(`\n[Browser] >>> NAVIGATION`);
170
+ debugLog(`[Browser] From: ${previousUrl}`);
171
+ debugLog(`[Browser] To: ${url}`);
172
+ debugLog(`[Browser] Segments to send: ${segments.join(", ")}`);
173
+ if (interceptSourceUrl) {
174
+ debugLog(`[Browser] Intercept context from: ${interceptSourceUrl}`);
175
+ }
166
176
  }
167
177
 
168
- // Get cached segments for merging with server diff.
169
- // When navigating with targetCacheSegments, use those for consistency.
170
- // Otherwise fall back to current page's segments (for same-route revalidation).
171
178
  const targetCache =
172
- mode.type === "navigate" ? mode.targetCacheSegments : undefined;
173
- const cachedSegs =
174
- targetCache && targetCache.length > 0
175
- ? targetCache
176
- : getCurrentCachedSegments();
179
+ mode.type === "navigate" && mode.targetCacheSegments?.length
180
+ ? mode.targetCacheSegments
181
+ : undefined;
182
+ const cachedSegs = targetCache ?? getCurrentCachedSegments();
183
+ const cachedSegsSource = targetCache ? "history-cache" : "current-page";
184
+ if (IS_BROWSER_DEBUG) {
185
+ debugLog(
186
+ `[Browser] cachedSegs source: ${cachedSegsSource} (${cachedSegs.length} segments: ${cachedSegs.map((s) => s.id).join(", ")})`,
187
+ );
188
+ }
177
189
 
178
- // Fetch partial payload (no abort signal - RSC doesn't support it well)
179
190
  let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
180
191
  fetchResult = await client.fetchPartial({
181
192
  targetUrl: url,
182
193
  segmentIds: segments,
183
194
  previousUrl,
184
- // Mark stale when explicitly requested OR when no segments are sent
185
- // (action redirect sends empty segments for a fresh render).
186
195
  staleRevalidation:
187
196
  mode.type === "stale-revalidation" || segments.length === 0,
188
- version,
197
+ version: getVersion(),
198
+ routerId: store.getRouterId?.(),
189
199
  });
190
- // Mark navigation as streaming (response received, now parsing RSC).
191
- // Called after fetchPartial so pendingUrl stays set during the network wait,
192
- // allowing useLinkStatus to show per-link pending indicators.
193
200
  const streamingToken = tx.startStreaming();
194
- const { payload, streamComplete: rawStreamComplete } = fetchResult;
201
+ const {
202
+ payload,
203
+ streamComplete: rawStreamComplete,
204
+ fullyPrefetched,
205
+ } = fetchResult;
195
206
  debugLog("payload.metadata", payload.metadata);
196
207
 
197
- const streamComplete = rawStreamComplete.then(() => {
198
- streamingToken.end();
199
- });
208
+ // Side effect only: end the streaming token once the stream settles.
209
+ // The wrapped promise was never read as a value; only the .end() matters.
210
+ // The .catch keeps an unhandled rejection from leaking if the stream errors.
211
+ rawStreamComplete.then(() => streamingToken.end()).catch(() => {});
212
+
213
+ const currentRouterId = store.getRouterId?.();
214
+ if (
215
+ payload.metadata?.routerId &&
216
+ currentRouterId &&
217
+ payload.metadata.routerId !== currentRouterId
218
+ ) {
219
+ console.error(
220
+ `[rango] Partial response router id "${payload.metadata.routerId}" does not ` +
221
+ `match this client ("${currentRouterId}"); discarding it and reloading to re-sync.`,
222
+ );
223
+ window.location.href = url;
224
+ return;
225
+ }
200
226
 
201
- // Handle server-side redirect with state
202
227
  if (payload.metadata?.redirect) {
203
228
  if (signal?.aborted) {
204
229
  debugLog("[Browser] Ignoring stale redirect (aborted)");
205
230
  return;
206
231
  }
232
+ // Explicit off-host redirect (redirect(url, { external: true })):
233
+ // hard-navigate, but still scheme-validate (http/https only). external
234
+ // waives the same-origin check the app opted out of, NOT scheme safety, so
235
+ // a forged payload carrying a javascript:/data: URL cannot script via
236
+ // location.assign.
237
+ if (payload.metadata.redirect.external) {
238
+ const externalUrl = validateExternalRedirect(
239
+ payload.metadata.redirect.url,
240
+ window.location.origin,
241
+ );
242
+ if (!externalUrl) {
243
+ debugLog("[Browser] Ignoring blocked external redirect payload");
244
+ return;
245
+ }
246
+ debugLog("[Browser] External redirect (hard navigation)");
247
+ window.location.assign(externalUrl);
248
+ return;
249
+ }
207
250
  const redirectUrl = validateRedirectOrigin(
208
251
  payload.metadata.redirect.url,
209
252
  window.location.origin,
@@ -225,10 +268,11 @@ export function createPartialUpdater(
225
268
  return;
226
269
  }
227
270
 
228
- debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
229
- debugLog(`[Browser] Diff: ${diff?.join(", ")}`);
271
+ if (IS_BROWSER_DEBUG) {
272
+ debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
273
+ debugLog(`[Browser] Diff: ${diff?.join(", ")}`);
274
+ }
230
275
 
231
- // If diff is empty, nothing changed on server side.
232
276
  if (!diff || diff.length === 0) {
233
277
  const matchedIds = matched || [];
234
278
  const cacheMap = new Map(cachedSegs.map((s) => [s.id, s]));
@@ -236,8 +280,7 @@ export function createPartialUpdater(
236
280
  .map((id: string) => cacheMap.get(id))
237
281
  .filter(Boolean) as ResolvedSegment[];
238
282
 
239
- // When navigating with cached segments to a different route, render them.
240
- if (mode.type === "navigate" && targetCache && targetCache.length > 0) {
283
+ if (mode.type === "navigate" && targetCache) {
241
284
  debugLog(
242
285
  "[Browser] No diff but navigating with cached segments - rendering target route",
243
286
  );
@@ -246,12 +289,18 @@ export function createPartialUpdater(
246
289
  forceAwait: true,
247
290
  });
248
291
 
249
- tx.commit(matchedIds, existingSegments);
292
+ const { scroll: commitScroll } = tx.commit(
293
+ matchedIds,
294
+ existingSegments,
295
+ );
296
+
297
+ if (mode.targetCacheHandleData) {
298
+ store.updateCacheHandleData(
299
+ store.getHistoryKey(),
300
+ mode.targetCacheHandleData,
301
+ );
302
+ }
250
303
 
251
- // Include cachedHandleData in metadata so NavigationProvider can restore
252
- // breadcrumbs and other handle data from cache.
253
- // Remove `handles` from metadata to prevent NavigationProvider from
254
- // processing an empty handles stream, which would clear the cached breadcrumbs.
255
304
  const { handles: _unusedHandles, ...metadataWithoutHandles } =
256
305
  payload.metadata!;
257
306
  const cachedUpdate = {
@@ -260,12 +309,10 @@ export function createPartialUpdater(
260
309
  ...metadataWithoutHandles,
261
310
  cachedHandleData: mode.targetCacheHandleData,
262
311
  },
312
+ scroll: toScrollPayload(commitScroll),
263
313
  };
264
314
 
265
- const cachedHasTransition = existingSegments.some(
266
- (s) => s.transition,
267
- );
268
- if (cachedHasTransition) {
315
+ if (shouldStartViewTransition(existingSegments)) {
269
316
  startTransition(() => {
270
317
  if (addTransitionType) {
271
318
  addTransitionType("navigation");
@@ -280,7 +327,6 @@ export function createPartialUpdater(
280
327
  return;
281
328
  }
282
329
 
283
- // When leaving intercept, force re-render even with empty diff
284
330
  if (mode.type === "leave-intercept") {
285
331
  debugLog(
286
332
  "[Browser] Leaving intercept - forcing re-render to remove modal",
@@ -290,18 +336,21 @@ export function createPartialUpdater(
290
336
  forceAwait: true,
291
337
  });
292
338
 
293
- tx.commit(matchedIds, existingSegments);
339
+ const { scroll: leaveScroll } = tx.commit(
340
+ matchedIds,
341
+ existingSegments,
342
+ );
294
343
 
295
344
  onUpdate({
296
345
  root: newTree,
297
346
  metadata: payload.metadata,
347
+ scroll: toScrollPayload(leaveScroll),
298
348
  });
299
349
 
300
350
  debugLog("[Browser] Navigation complete (left intercept)");
301
351
  return;
302
352
  }
303
353
 
304
- // Same route revalidation with no changes - skip UI update
305
354
  debugLog(
306
355
  "[Browser] No changes - all revalidations returned false, keeping existing UI",
307
356
  );
@@ -310,7 +359,6 @@ export function createPartialUpdater(
310
359
  return;
311
360
  }
312
361
 
313
- // Reconcile server segments with cached segments (single source of truth)
314
362
  const matchedIds = matched || [];
315
363
  const actor: ReconcileActor =
316
364
  mode.type === "stale-revalidation" || mode.type === "action"
@@ -326,7 +374,6 @@ export function createPartialUpdater(
326
374
  insertMissingDiff: true,
327
375
  });
328
376
 
329
- // HMR RESILIENCE: Check if we're missing any matched segments
330
377
  const reconciledIdSet = new Set(reconciled.segments.map((s) => s.id));
331
378
  const missingIds = matchedIds.filter(
332
379
  (id: string) => !reconciledIdSet.has(id),
@@ -348,13 +395,19 @@ export function createPartialUpdater(
348
395
  return;
349
396
  }
350
397
  if (mode.type === "action") {
398
+ // An action refetch that lands on missing segments (navigated away /
399
+ // consolidation / HMR) drops rather than refetch-all: the action flow
400
+ // is storeOnly / skipLoadingState, so a full refetch here would fight
401
+ // it. Keep the stale-but-consistent tree; log so the drop is visible.
402
+ debugLog(
403
+ `[Browser] Action refetch: ${missingCount} segments missing; dropping (stale-but-consistent tree kept).`,
404
+ );
351
405
  return;
352
406
  }
353
407
  console.warn(
354
408
  `[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
355
409
  );
356
410
 
357
- // Refetch with empty segments = server sends everything
358
411
  return fetchPartialUpdate(url, [], true, signal, tx, mode);
359
412
  }
360
413
 
@@ -363,44 +416,59 @@ export function createPartialUpdater(
363
416
  return;
364
417
  }
365
418
 
366
- // Rebuild tree on client (await for loader data resolution)
367
419
  const renderOptions = {
368
420
  isAction: mode.type === "action",
369
- forceAwait: mode.type === "stale-revalidation",
421
+ // forceAwait unwraps the ROUTER loader promises during render so they
422
+ // land without a loading()/fallback frame. A fully-prefetched nav has
423
+ // its router data already resolved (the prefetch stream drained), so
424
+ // awaiting it here is free and lets us commit NORMALLY (not in a
425
+ // transition) below — a normal commit still shows fallbacks for any
426
+ // CLIENT component that suspends on mount, which a transition would
427
+ // wrongly suppress by holding the old UI until that suspense settles.
428
+ forceAwait: mode.type === "stale-revalidation" || fullyPrefetched,
370
429
  interceptSegments:
371
430
  reconciled.interceptSegments.length > 0
372
431
  ? reconciled.interceptSegments
373
432
  : undefined,
374
433
  };
375
- const newTree = await (signal
376
- ? Promise.race([
434
+ let newTree: Awaited<ReturnType<typeof renderSegments>>;
435
+ if (signal) {
436
+ // Race render against abort. Store the abort handler and register it
437
+ // { once:true } so a non-aborted render (which wins the race) can
438
+ // remove it in finally — otherwise the listener stays attached and the
439
+ // rejecting promise never settles. Mirrors teeWithCompletion in
440
+ // browser/response-adapter.ts.
441
+ let onAbort: (() => void) | undefined;
442
+ const abortPromise = new Promise<never>((_, reject) => {
443
+ if (signal.aborted) {
444
+ reject(new DOMException("Navigation aborted", "AbortError"));
445
+ return;
446
+ }
447
+ onAbort = () =>
448
+ reject(new DOMException("Navigation aborted", "AbortError"));
449
+ signal.addEventListener("abort", onAbort, { once: true });
450
+ });
451
+ try {
452
+ newTree = await Promise.race([
377
453
  renderSegments(reconciled.mainSegments, renderOptions),
378
- new Promise<never>((_, reject) => {
379
- if (signal.aborted) {
380
- reject(new DOMException("Navigation aborted", "AbortError"));
381
- }
382
- signal.addEventListener("abort", () => {
383
- reject(new DOMException("Navigation aborted", "AbortError"));
384
- });
385
- }),
386
- ])
387
- : renderSegments(reconciled.mainSegments, renderOptions));
388
-
389
- // Final abort check before committing - another navigation may have started
454
+ abortPromise,
455
+ ]);
456
+ } finally {
457
+ if (onAbort) signal.removeEventListener("abort", onAbort);
458
+ }
459
+ } else {
460
+ newTree = await renderSegments(reconciled.mainSegments, renderOptions);
461
+ }
462
+
390
463
  if (signal?.aborted) {
391
464
  debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
392
465
  return;
393
466
  }
394
467
 
395
- // Check if this is an intercept response (any slot is active)
396
468
  const isInterceptResponse = hasActiveInterceptSlots(
397
469
  payload.metadata?.slots,
398
470
  );
399
471
 
400
- // Track intercept context (only on navigation, not actions or stale revalidation)
401
- // Use the authoritative source from mode/history state when restoring an
402
- // intercept via popstate cache miss; fall back to the current URL for fresh
403
- // intercept navigations.
404
472
  const effectiveInterceptSource =
405
473
  interceptSourceUrl || segmentState.currentUrl;
406
474
  if (mode.type !== "action" && mode.type !== "stale-revalidation") {
@@ -411,9 +479,6 @@ export function createPartialUpdater(
411
479
  }
412
480
  }
413
481
 
414
- // Commit navigation - use server's matched as the authoritative segment ID list.
415
- // reconciled.segments may be missing IDs (e.g., loader segments not in diff or cache)
416
- // but the server's matched always includes all expected segment IDs.
417
482
  const allSegmentIds = matchedIds;
418
483
  const serverLocationState = payload.metadata?.locationState;
419
484
  const overrides: CommitOverrides | undefined = isInterceptResponse
@@ -426,9 +491,12 @@ export function createPartialUpdater(
426
491
  : serverLocationState
427
492
  ? { serverState: serverLocationState }
428
493
  : undefined;
429
- tx.commit(allSegmentIds, reconciled.segments, overrides);
494
+ const { scroll: navScroll } = tx.commit(
495
+ allSegmentIds,
496
+ reconciled.segments,
497
+ overrides,
498
+ );
430
499
 
431
- // For stale revalidation: verify history key hasn't changed before updating UI
432
500
  if (mode.type === "stale-revalidation") {
433
501
  const historyKeyNow = store.getHistoryKey();
434
502
  if (historyKeyNow !== historyKeyAtStart) {
@@ -441,8 +509,23 @@ export function createPartialUpdater(
441
509
 
442
510
  debugLog("[partial-update] updating document");
443
511
 
444
- // Emit update to trigger React render
445
- const hasTransition = reconciled.mainSegments.some((s) => s.transition);
512
+ const hasTransition = shouldStartViewTransition(reconciled.segments);
513
+ // [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. Reports which reconciled
514
+ // segment still carries a transition after the server-side when-gate, and
515
+ // whether the commit will be held in a startTransition. If `withTransition`
516
+ // lists an ancestor (layout/root) id rather than the gated leaf, an ungated
517
+ // ancestor transition is holding the subtree (missing loading() fallback).
518
+ if (isBrowserDebugEnabled()) {
519
+ debugLog("[VT-DIAG] commit", {
520
+ mode: mode.type,
521
+ hasTransition,
522
+ withTransition: reconciled.segments
523
+ .filter((s) => s.transition)
524
+ .map((s) => s.id),
525
+ all: reconciled.segments.map((s) => s.id),
526
+ });
527
+ }
528
+ const scrollPayload = toScrollPayload(navScroll);
446
529
 
447
530
  if (mode.type === "action" || mode.type === "stale-revalidation") {
448
531
  startTransition(() => {
@@ -452,6 +535,7 @@ export function createPartialUpdater(
452
535
  onUpdate({
453
536
  root: newTree,
454
537
  metadata: payload.metadata!,
538
+ scroll: scrollPayload,
455
539
  });
456
540
  });
457
541
  } else if (hasTransition) {
@@ -462,19 +546,34 @@ export function createPartialUpdater(
462
546
  onUpdate({
463
547
  root: newTree,
464
548
  metadata: payload.metadata!,
549
+ scroll: scrollPayload,
465
550
  });
466
551
  });
467
552
  } else {
553
+ // Normal commit (cold/partial nav AND fully-prefetched nav). For a
554
+ // fully-prefetched nav, renderOptions.forceAwait (above) unwrapped the
555
+ // already-resolved ROUTER loader data AND route content during render, so
556
+ // the new tree carries it inline with no loading()/fallback frame — yet we
557
+ // still commit NORMALLY here rather than in a transition. A transition
558
+ // holds the OLD UI until ALL suspense in the new tree settles, including a
559
+ // CLIENT component that starts its own data request only when mounted
560
+ // (post-commit) under a persistent boundary; that would retain the
561
+ // previous page indefinitely with no feedback. A normal commit lets such
562
+ // client-initiated suspense reveal a fallback (correct) while the router
563
+ // data — genuinely ready — never flashes. Cold/partial navs
564
+ // (fullyPrefetched=false) do not forceAwait, so they stream their
565
+ // fallbacks. Explicit transition() routes keep the broader content-hold
566
+ // via the hasTransition branch above (the documented opt-in).
468
567
  onUpdate({
469
568
  root: newTree,
470
569
  metadata: payload.metadata!,
570
+ scroll: scrollPayload,
471
571
  });
472
572
  }
473
573
 
474
574
  debugLog("[Browser] Navigation complete");
475
575
  return;
476
576
  } else {
477
- // Full update (fallback)
478
577
  console.warn(`[Browser] Full update (fallback)`);
479
578
 
480
579
  const segments = payload.metadata?.segments || [];
@@ -494,18 +593,29 @@ export function createPartialUpdater(
494
593
  }
495
594
 
496
595
  const fullUpdateServerState = payload.metadata?.locationState;
497
- if (fullUpdateServerState) {
498
- tx.commit(segmentIds, segments, { serverState: fullUpdateServerState });
499
- } else {
500
- tx.commit(segmentIds, segments);
501
- }
596
+ const { scroll: fullScroll } = fullUpdateServerState
597
+ ? tx.commit(segmentIds, segments, {
598
+ serverState: fullUpdateServerState,
599
+ })
600
+ : tx.commit(segmentIds, segments);
502
601
 
503
- const fullHasTransition = segments.some(
504
- (s: ResolvedSegment) => s.transition,
505
- );
602
+ const fullHasTransition = shouldStartViewTransition(segments);
603
+ const fullScrollPayload = toScrollPayload(fullScroll);
506
604
 
507
605
  if (mode.type === "stale-revalidation") {
508
606
  await rawStreamComplete;
607
+ // Mirror the partial branch's history-key staleness guard (above): the
608
+ // await above is a real async suspension, so the user may have navigated
609
+ // away while this background revalidation was draining. Dropping a late
610
+ // full-update here prevents it from clobbering the freshly committed UI
611
+ // of the page the user moved to.
612
+ const historyKeyNow = store.getHistoryKey();
613
+ if (historyKeyNow !== historyKeyAtStart) {
614
+ debugLog(
615
+ `[Browser] Stale revalidation (full update): history key changed (${historyKeyAtStart} -> ${historyKeyNow}), skipping UI update`,
616
+ );
617
+ return;
618
+ }
509
619
  startTransition(() => {
510
620
  if (fullHasTransition && addTransitionType) {
511
621
  addTransitionType("action");
@@ -513,16 +623,18 @@ export function createPartialUpdater(
513
623
  onUpdate({
514
624
  root: newTree,
515
625
  metadata: payload.metadata!,
626
+ scroll: fullScrollPayload,
516
627
  });
517
628
  });
518
629
  } else if (mode.type === "action") {
519
- startTransition(async () => {
630
+ startTransition(() => {
520
631
  if (fullHasTransition && addTransitionType) {
521
632
  addTransitionType("action");
522
633
  }
523
634
  onUpdate({
524
635
  root: newTree,
525
636
  metadata: payload.metadata!,
637
+ scroll: fullScrollPayload,
526
638
  });
527
639
  });
528
640
  } else if (fullHasTransition) {
@@ -533,12 +645,14 @@ export function createPartialUpdater(
533
645
  onUpdate({
534
646
  root: newTree,
535
647
  metadata: payload.metadata!,
648
+ scroll: fullScrollPayload,
536
649
  });
537
650
  });
538
651
  } else {
539
652
  onUpdate({
540
653
  root: newTree,
541
654
  metadata: payload.metadata!,
655
+ scroll: fullScrollPayload,
542
656
  });
543
657
  }
544
658