@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7c7e4327

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 (440) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +301 -797
  3. package/dist/bin/rango.js +603 -145
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3750 -1160
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +96 -24
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +85 -6
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +228 -33
  12. package/skills/caching/SKILL.md +336 -19
  13. package/skills/catalog.json +271 -0
  14. package/skills/comparison/SKILL.md +50 -0
  15. package/skills/comparison/agents/openai.yaml +4 -0
  16. package/skills/comparison/references/framework-comparison.md +837 -0
  17. package/skills/composability/SKILL.md +110 -4
  18. package/skills/css/SKILL.md +76 -0
  19. package/skills/debug-manifest/SKILL.md +5 -3
  20. package/skills/defer-hydration/SKILL.md +235 -0
  21. package/skills/document-cache/SKILL.md +87 -56
  22. package/skills/fonts/SKILL.md +1 -1
  23. package/skills/handler-use/SKILL.md +12 -10
  24. package/skills/hooks/SKILL.md +73 -691
  25. package/skills/hooks/data.md +273 -0
  26. package/skills/hooks/handle-and-actions.md +103 -0
  27. package/skills/hooks/navigation.md +110 -0
  28. package/skills/hooks/outlets.md +41 -0
  29. package/skills/hooks/state.md +228 -0
  30. package/skills/hooks/urls.md +135 -0
  31. package/skills/host-router/SKILL.md +129 -27
  32. package/skills/i18n/SKILL.md +276 -0
  33. package/skills/intercept/SKILL.md +75 -19
  34. package/skills/layout/SKILL.md +40 -19
  35. package/skills/links/SKILL.md +247 -17
  36. package/skills/loader/SKILL.md +248 -10
  37. package/skills/middleware/SKILL.md +25 -13
  38. package/skills/migrate-nextjs/SKILL.md +205 -20
  39. package/skills/migrate-react-router/SKILL.md +59 -670
  40. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  41. package/skills/migrate-react-router/component-migration.md +196 -0
  42. package/skills/migrate-react-router/data-and-actions.md +225 -0
  43. package/skills/migrate-react-router/route-mapping.md +271 -0
  44. package/skills/mime-routes/SKILL.md +29 -2
  45. package/skills/observability/SKILL.md +202 -0
  46. package/skills/parallel/SKILL.md +40 -10
  47. package/skills/ppr/SKILL.md +616 -0
  48. package/skills/prerender/SKILL.md +72 -60
  49. package/skills/rango/SKILL.md +318 -26
  50. package/skills/react-compiler/SKILL.md +168 -0
  51. package/skills/response-routes/SKILL.md +138 -49
  52. package/skills/route/SKILL.md +117 -9
  53. package/skills/router-setup/SKILL.md +44 -9
  54. package/skills/scripts/SKILL.md +179 -0
  55. package/skills/server-actions/SKILL.md +776 -0
  56. package/skills/shell-manifest/SKILL.md +185 -0
  57. package/skills/streams-and-websockets/SKILL.md +283 -0
  58. package/skills/tailwind/SKILL.md +28 -4
  59. package/skills/testing/SKILL.md +130 -0
  60. package/skills/testing/bindings.md +103 -0
  61. package/skills/testing/cache-prerender.md +127 -0
  62. package/skills/testing/client-components.md +124 -0
  63. package/skills/testing/e2e-parity.md +125 -0
  64. package/skills/testing/flight.md +91 -0
  65. package/skills/testing/handles.md +131 -0
  66. package/skills/testing/loader.md +128 -0
  67. package/skills/testing/middleware.md +99 -0
  68. package/skills/testing/render-handler.md +122 -0
  69. package/skills/testing/response-routes.md +95 -0
  70. package/skills/testing/reverse-and-types.md +85 -0
  71. package/skills/testing/server-actions.md +107 -0
  72. package/skills/testing/server-tree.md +128 -0
  73. package/skills/testing/setup.md +123 -0
  74. package/skills/theme/SKILL.md +1 -1
  75. package/skills/typesafety/SKILL.md +45 -626
  76. package/skills/typesafety/env-and-bindings.md +254 -0
  77. package/skills/typesafety/generated-files-and-cli.md +335 -0
  78. package/skills/typesafety/params-and-search.md +153 -0
  79. package/skills/typesafety/route-types.md +209 -0
  80. package/skills/use-cache/SKILL.md +74 -15
  81. package/skills/vercel/SKILL.md +128 -0
  82. package/skills/view-transitions/SKILL.md +337 -0
  83. package/src/__augment-tests__/augment.ts +81 -0
  84. package/src/__augment-tests__/augmented.check.ts +116 -0
  85. package/src/__internal.ts +0 -65
  86. package/src/browser/action-coordinator.ts +53 -36
  87. package/src/browser/action-fence.ts +47 -0
  88. package/src/browser/app-shell.ts +39 -0
  89. package/src/browser/connection-warmup.ts +134 -0
  90. package/src/browser/cookie-name.ts +140 -0
  91. package/src/browser/event-controller.ts +252 -158
  92. package/src/browser/history-state.ts +21 -0
  93. package/src/browser/index.ts +3 -3
  94. package/src/browser/invalidate-client-cache.ts +52 -0
  95. package/src/browser/logging.ts +28 -0
  96. package/src/browser/merge-segment-loaders.ts +6 -4
  97. package/src/browser/navigation-bridge.ts +94 -25
  98. package/src/browser/navigation-client.ts +144 -79
  99. package/src/browser/navigation-store-handle.ts +38 -0
  100. package/src/browser/navigation-store.ts +161 -73
  101. package/src/browser/navigation-transaction.ts +9 -59
  102. package/src/browser/network-error-handler.ts +34 -7
  103. package/src/browser/partial-update.ts +183 -144
  104. package/src/browser/prefetch/cache.ts +242 -77
  105. package/src/browser/prefetch/fetch.ts +325 -69
  106. package/src/browser/prefetch/queue.ts +61 -12
  107. package/src/browser/rango-state.ts +158 -76
  108. package/src/browser/react/Link.tsx +58 -20
  109. package/src/browser/react/NavigationProvider.tsx +202 -120
  110. package/src/browser/react/ScrollRestoration.tsx +10 -6
  111. package/src/browser/react/filter-segment-order.ts +66 -7
  112. package/src/browser/react/index.ts +0 -48
  113. package/src/browser/react/location-state-shared.ts +178 -8
  114. package/src/browser/react/location-state.ts +39 -14
  115. package/src/browser/react/use-action.ts +6 -15
  116. package/src/browser/react/use-handle.ts +17 -14
  117. package/src/browser/react/use-href.tsx +8 -1
  118. package/src/browser/react/use-link-status.ts +33 -8
  119. package/src/browser/react/use-navigation.ts +32 -7
  120. package/src/browser/react/use-params.ts +20 -10
  121. package/src/browser/react/use-reverse.ts +106 -0
  122. package/src/browser/react/use-router.ts +25 -3
  123. package/src/browser/react/use-search-params.ts +0 -5
  124. package/src/browser/react/use-segments.ts +11 -21
  125. package/src/browser/response-adapter.ts +99 -8
  126. package/src/browser/rsc-router.tsx +145 -28
  127. package/src/browser/scroll-restoration.ts +37 -22
  128. package/src/browser/segment-reconciler.ts +31 -21
  129. package/src/browser/segment-structure-assert.ts +2 -2
  130. package/src/browser/server-action-bridge.ts +236 -65
  131. package/src/browser/types.ts +102 -9
  132. package/src/browser/validate-redirect-origin.ts +43 -16
  133. package/src/build/collect-fallback-refs.ts +107 -0
  134. package/src/build/generate-manifest.ts +203 -154
  135. package/src/build/generate-route-types.ts +3 -1
  136. package/src/build/index.ts +11 -3
  137. package/src/build/prefix-tree-utils.ts +123 -0
  138. package/src/build/route-trie.ts +152 -21
  139. package/src/build/route-types/ast-route-extraction.ts +15 -8
  140. package/src/build/route-types/codegen.ts +16 -5
  141. package/src/build/route-types/include-resolution.ts +456 -62
  142. package/src/build/route-types/param-extraction.ts +6 -3
  143. package/src/build/route-types/per-module-writer.ts +22 -6
  144. package/src/build/route-types/router-processing.ts +128 -51
  145. package/src/build/route-types/scan-filter.ts +1 -1
  146. package/src/build/route-types/source-scan.ts +216 -0
  147. package/src/build/runtime-discovery.ts +13 -21
  148. package/src/cache/cache-error.ts +104 -0
  149. package/src/cache/cache-key-utils.ts +58 -13
  150. package/src/cache/cache-policy.ts +108 -34
  151. package/src/cache/cache-runtime.ts +421 -58
  152. package/src/cache/cache-scope.ts +187 -96
  153. package/src/cache/cache-tag.ts +149 -0
  154. package/src/cache/cf/cf-base64.ts +33 -0
  155. package/src/cache/cf/cf-cache-constants.ts +127 -0
  156. package/src/cache/cf/cf-cache-store.ts +2202 -372
  157. package/src/cache/cf/cf-cache-types.ts +349 -0
  158. package/src/cache/cf/cf-kv-utils.ts +46 -0
  159. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  160. package/src/cache/cf/index.ts +6 -16
  161. package/src/cache/document-cache.ts +126 -41
  162. package/src/cache/handle-snapshot.ts +70 -0
  163. package/src/cache/index.ts +23 -20
  164. package/src/cache/memory-segment-store.ts +243 -37
  165. package/src/cache/profile-registry.ts +46 -31
  166. package/src/cache/read-through-swr.ts +56 -12
  167. package/src/cache/segment-codec.ts +13 -21
  168. package/src/cache/shell-snapshot.ts +417 -0
  169. package/src/cache/tag-invalidation.ts +230 -0
  170. package/src/cache/types.ts +180 -99
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1127 -0
  173. package/src/client.rsc.tsx +41 -21
  174. package/src/client.tsx +33 -61
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +108 -0
  177. package/src/component-utils.ts +19 -0
  178. package/src/components/DefaultDocument.tsx +8 -2
  179. package/src/context-var.ts +18 -6
  180. package/src/decode-loader-results.ts +52 -0
  181. package/src/defer.ts +185 -0
  182. package/src/deps/ssr.ts +0 -1
  183. package/src/encode-kv.ts +49 -0
  184. package/src/errors.ts +30 -4
  185. package/src/escape-script.ts +52 -0
  186. package/src/handle.ts +67 -37
  187. package/src/handles/MetaTags.tsx +24 -53
  188. package/src/handles/Scripts.tsx +183 -0
  189. package/src/handles/breadcrumbs.ts +35 -8
  190. package/src/handles/deferred-resolution.ts +127 -0
  191. package/src/handles/is-thenable.ts +18 -0
  192. package/src/handles/meta.ts +14 -40
  193. package/src/handles/script.ts +244 -0
  194. package/src/host/cookie-handler.ts +9 -60
  195. package/src/host/errors.ts +13 -22
  196. package/src/host/index.ts +9 -2
  197. package/src/host/pattern-matcher.ts +23 -52
  198. package/src/host/router.ts +107 -99
  199. package/src/host/testing.ts +40 -27
  200. package/src/host/types.ts +37 -4
  201. package/src/host/utils.ts +1 -1
  202. package/src/href-client.ts +137 -22
  203. package/src/index.rsc.ts +97 -12
  204. package/src/index.ts +98 -14
  205. package/src/internal-debug.ts +11 -10
  206. package/src/loader-store.ts +500 -0
  207. package/src/loader.rsc.ts +20 -13
  208. package/src/loader.ts +12 -11
  209. package/src/missing-id-error.ts +68 -0
  210. package/src/outlet-context.ts +1 -1
  211. package/src/outlet-provider.tsx +1 -5
  212. package/src/prerender/param-hash.ts +16 -16
  213. package/src/prerender/store.ts +32 -37
  214. package/src/prerender.ts +78 -10
  215. package/src/redirect-origin.ts +114 -0
  216. package/src/regex-escape.ts +8 -0
  217. package/src/render-error-thrower.tsx +20 -0
  218. package/src/response-utils.ts +62 -0
  219. package/src/reverse.ts +65 -39
  220. package/src/root-error-boundary.tsx +1 -19
  221. package/src/route-content-wrapper.tsx +19 -77
  222. package/src/route-definition/dsl-helpers.ts +304 -309
  223. package/src/route-definition/helper-factories.ts +28 -140
  224. package/src/route-definition/helpers-types.ts +87 -59
  225. package/src/route-definition/index.ts +1 -2
  226. package/src/route-definition/redirect.ts +44 -11
  227. package/src/route-definition/resolve-handler-use.ts +12 -1
  228. package/src/route-definition/use-item-types.ts +29 -0
  229. package/src/route-map-builder.ts +41 -20
  230. package/src/route-types.ts +19 -46
  231. package/src/router/basename.ts +14 -0
  232. package/src/router/content-negotiation.ts +73 -25
  233. package/src/router/error-handling.ts +45 -18
  234. package/src/router/find-match.ts +129 -30
  235. package/src/router/handler-context.ts +27 -42
  236. package/src/router/instrument.ts +355 -0
  237. package/src/router/intercept-resolution.ts +39 -20
  238. package/src/router/lazy-includes.ts +82 -59
  239. package/src/router/loader-resolution.ts +167 -72
  240. package/src/router/logging.ts +0 -6
  241. package/src/router/manifest.ts +74 -40
  242. package/src/router/match-api.ts +80 -55
  243. package/src/router/match-context.ts +0 -22
  244. package/src/router/match-handlers.ts +211 -165
  245. package/src/router/match-middleware/background-revalidation.ts +40 -24
  246. package/src/router/match-middleware/cache-lookup.ts +159 -285
  247. package/src/router/match-middleware/cache-store.ts +64 -52
  248. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  249. package/src/router/match-middleware/segment-resolution.ts +0 -22
  250. package/src/router/match-pipelines.ts +1 -42
  251. package/src/router/match-result.ts +69 -79
  252. package/src/router/metrics.ts +0 -34
  253. package/src/router/middleware-types.ts +7 -134
  254. package/src/router/middleware.ts +298 -172
  255. package/src/router/navigation-snapshot.ts +7 -56
  256. package/src/router/params-util.ts +23 -0
  257. package/src/router/parse-pattern.ts +115 -0
  258. package/src/router/pattern-matching.ts +181 -150
  259. package/src/router/prefetch-cache-ttl.ts +51 -0
  260. package/src/router/prefetch-limits.ts +37 -0
  261. package/src/router/prerender-match.ts +112 -67
  262. package/src/router/preview-match.ts +6 -2
  263. package/src/router/request-classification.ts +50 -69
  264. package/src/router/revalidation.ts +123 -73
  265. package/src/router/route-snapshot.ts +14 -3
  266. package/src/router/router-context.ts +6 -29
  267. package/src/router/router-interfaces.ts +115 -36
  268. package/src/router/router-options.ts +166 -5
  269. package/src/router/router-registry.ts +2 -5
  270. package/src/router/segment-resolution/fresh.ts +131 -86
  271. package/src/router/segment-resolution/helpers.ts +86 -6
  272. package/src/router/segment-resolution/loader-cache.ts +139 -39
  273. package/src/router/segment-resolution/loader-mask.ts +67 -0
  274. package/src/router/segment-resolution/loader-snapshot.ts +251 -0
  275. package/src/router/segment-resolution/revalidation.ts +272 -320
  276. package/src/router/segment-resolution/static-store.ts +19 -5
  277. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  278. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  279. package/src/router/segment-resolution.ts +5 -1
  280. package/src/router/segment-wrappers.ts +6 -5
  281. package/src/router/state-cookie-name.ts +33 -0
  282. package/src/router/substitute-pattern-params.ts +75 -0
  283. package/src/router/telemetry-otel.ts +160 -200
  284. package/src/router/telemetry.ts +105 -20
  285. package/src/router/timeout.ts +0 -20
  286. package/src/router/tracing.ts +215 -0
  287. package/src/router/trie-matching.ts +171 -59
  288. package/src/router/types.ts +9 -63
  289. package/src/router/url-params.ts +57 -0
  290. package/src/router.ts +157 -71
  291. package/src/rsc/full-payload.ts +70 -0
  292. package/src/rsc/handler-context.ts +3 -2
  293. package/src/rsc/handler.ts +291 -217
  294. package/src/rsc/helpers.ts +168 -46
  295. package/src/rsc/index.ts +2 -5
  296. package/src/rsc/json-route-result.ts +38 -0
  297. package/src/rsc/loader-fetch.ts +114 -38
  298. package/src/rsc/manifest-init.ts +29 -42
  299. package/src/rsc/nonce.ts +10 -1
  300. package/src/rsc/origin-guard.ts +39 -25
  301. package/src/rsc/progressive-enhancement.ts +124 -13
  302. package/src/rsc/redirect-guard.ts +100 -0
  303. package/src/rsc/response-cache-serve.ts +238 -0
  304. package/src/rsc/response-error.ts +79 -12
  305. package/src/rsc/response-route-handler.ts +99 -189
  306. package/src/rsc/rsc-rendering.ts +421 -76
  307. package/src/rsc/runtime-warnings.ts +23 -10
  308. package/src/rsc/server-action.ts +282 -116
  309. package/src/rsc/shell-capture.ts +1158 -0
  310. package/src/rsc/shell-serve.ts +150 -0
  311. package/src/rsc/ssr-setup.ts +16 -0
  312. package/src/rsc/transition-gate.ts +89 -0
  313. package/src/rsc/types.ts +53 -5
  314. package/src/runtime-env.ts +18 -0
  315. package/src/search-params.ts +35 -30
  316. package/src/segment-loader-promise.ts +49 -4
  317. package/src/segment-system.tsx +350 -149
  318. package/src/serialize.ts +243 -0
  319. package/src/server/context.ts +208 -51
  320. package/src/server/cookie-parse.ts +32 -0
  321. package/src/server/cookie-store.ts +152 -5
  322. package/src/server/handle-store.ts +21 -38
  323. package/src/server/loader-registry.ts +33 -42
  324. package/src/server/request-context.ts +395 -176
  325. package/src/ssr/index.tsx +458 -178
  326. package/src/ssr/ssr-root.tsx +228 -0
  327. package/src/static-handler.ts +10 -13
  328. package/src/testing/cache-status.ts +162 -0
  329. package/src/testing/collect-handle.ts +46 -0
  330. package/src/testing/dispatch.ts +813 -0
  331. package/src/testing/dom.entry.ts +22 -0
  332. package/src/testing/e2e/fixture.ts +188 -0
  333. package/src/testing/e2e/index.ts +128 -0
  334. package/src/testing/e2e/matchers.ts +35 -0
  335. package/src/testing/e2e/page-helpers.ts +272 -0
  336. package/src/testing/e2e/parity.ts +387 -0
  337. package/src/testing/e2e/server.ts +195 -0
  338. package/src/testing/flight-matchers.ts +97 -0
  339. package/src/testing/flight-normalize.ts +11 -0
  340. package/src/testing/flight-runtime.d.ts +57 -0
  341. package/src/testing/flight-tree.ts +682 -0
  342. package/src/testing/flight.entry.ts +52 -0
  343. package/src/testing/flight.ts +257 -0
  344. package/src/testing/generated-routes.ts +199 -0
  345. package/src/testing/index.ts +105 -0
  346. package/src/testing/internal/context.ts +371 -0
  347. package/src/testing/internal/flight-client-globals.ts +30 -0
  348. package/src/testing/internal/seed-vars.ts +54 -0
  349. package/src/testing/render-handler.ts +357 -0
  350. package/src/testing/render-route.tsx +584 -0
  351. package/src/testing/run-loader.ts +385 -0
  352. package/src/testing/run-middleware.ts +205 -0
  353. package/src/testing/run-transition-when.ts +164 -0
  354. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  355. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  356. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  357. package/src/testing/vitest-stubs/version.ts +5 -0
  358. package/src/testing/vitest.ts +305 -0
  359. package/src/theme/ThemeProvider.tsx +56 -84
  360. package/src/theme/ThemeScript.tsx +7 -9
  361. package/src/theme/constants.ts +52 -13
  362. package/src/theme/index.ts +0 -7
  363. package/src/theme/theme-context.ts +1 -5
  364. package/src/theme/theme-script.ts +22 -21
  365. package/src/theme/use-theme.ts +0 -3
  366. package/src/types/boundaries.ts +0 -35
  367. package/src/types/cache-types.ts +13 -4
  368. package/src/types/error-types.ts +30 -90
  369. package/src/types/global-namespace.ts +54 -41
  370. package/src/types/handler-context.ts +110 -62
  371. package/src/types/index.ts +3 -10
  372. package/src/types/loader-types.ts +11 -9
  373. package/src/types/request-scope.ts +112 -0
  374. package/src/types/route-config.ts +20 -52
  375. package/src/types/route-entry.ts +0 -6
  376. package/src/types/segments.ts +135 -14
  377. package/src/urls/include-helper.ts +19 -64
  378. package/src/urls/include-provider.ts +71 -0
  379. package/src/urls/index.ts +2 -11
  380. package/src/urls/path-helper-types.ts +63 -17
  381. package/src/urls/path-helper.ts +22 -106
  382. package/src/urls/pattern-types.ts +72 -19
  383. package/src/urls/response-types.ts +22 -29
  384. package/src/urls/type-extraction.ts +98 -154
  385. package/src/urls/urls-function.ts +1 -19
  386. package/src/use-loader.tsx +292 -107
  387. package/src/vercel/index.ts +11 -0
  388. package/src/vercel/tracing.ts +88 -0
  389. package/src/vite/debug.ts +185 -0
  390. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  391. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  392. package/src/vite/discovery/discover-routers.ts +127 -86
  393. package/src/vite/discovery/discovery-errors.ts +255 -0
  394. package/src/vite/discovery/gate-state.ts +171 -0
  395. package/src/vite/discovery/prerender-collection.ts +96 -68
  396. package/src/vite/discovery/route-types-writer.ts +40 -84
  397. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  398. package/src/vite/discovery/state.ts +45 -1
  399. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  400. package/src/vite/index.ts +4 -0
  401. package/src/vite/inject-client-debug.ts +88 -0
  402. package/src/vite/plugin-types.ts +210 -10
  403. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  404. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  405. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  406. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  407. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  408. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  409. package/src/vite/plugins/expose-action-id.ts +48 -95
  410. package/src/vite/plugins/expose-id-utils.ts +88 -55
  411. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  412. package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
  413. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  414. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  415. package/src/vite/plugins/expose-internal-ids.ts +505 -486
  416. package/src/vite/plugins/performance-tracks.ts +26 -25
  417. package/src/vite/plugins/refresh-cmd.ts +1 -1
  418. package/src/vite/plugins/use-cache-transform.ts +73 -83
  419. package/src/vite/plugins/vercel-output.ts +384 -0
  420. package/src/vite/plugins/version-injector.ts +40 -29
  421. package/src/vite/plugins/version-plugin.ts +37 -40
  422. package/src/vite/plugins/virtual-entries.ts +138 -27
  423. package/src/vite/rango.ts +236 -138
  424. package/src/vite/router-discovery.ts +927 -136
  425. package/src/vite/utils/ast-handler-extract.ts +26 -35
  426. package/src/vite/utils/banner.ts +1 -1
  427. package/src/vite/utils/bundle-analysis.ts +10 -15
  428. package/src/vite/utils/client-chunks.ts +184 -0
  429. package/src/vite/utils/directive-prologue.ts +40 -0
  430. package/src/vite/utils/forward-user-plugins.ts +171 -0
  431. package/src/vite/utils/manifest-utils.ts +4 -59
  432. package/src/vite/utils/package-resolution.ts +20 -52
  433. package/src/vite/utils/prerender-utils.ts +71 -43
  434. package/src/vite/utils/shared-utils.ts +142 -43
  435. package/src/browser/action-response-classifier.ts +0 -99
  436. package/src/browser/react/use-client-cache.ts +0 -58
  437. package/src/browser/shallow.ts +0 -40
  438. package/src/handles/index.ts +0 -7
  439. package/src/network-error-thrower.tsx +0 -23
  440. package/src/router/middleware-cookies.ts +0 -55
@@ -14,20 +14,39 @@ 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";
22
32
  import type { NavigationUpdate } from "./types.js";
23
33
 
24
- /** Build a scroll payload from the commit's scroll option */
25
34
  function toScrollPayload(
26
35
  scroll: boolean | undefined,
27
36
  ): NonNullable<NavigationUpdate["scroll"]> {
28
37
  return { enabled: scroll !== false ? scroll : false };
29
38
  }
30
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
+ }
49
+
31
50
  /**
32
51
  * Configuration for creating a partial updater
33
52
  */
@@ -76,7 +95,7 @@ export type UpdateMode =
76
95
  /** Source URL for intercept restore (popstate cache miss) */
77
96
  interceptSourceUrl?: string;
78
97
  }
79
- | { type: "leave-intercept" }
98
+ | { type: "leave-intercept"; interceptSourceUrl?: string }
80
99
  | { type: "stale-revalidation"; interceptSourceUrl?: string }
81
100
  | { type: "action"; interceptSourceUrl?: string };
82
101
 
@@ -92,15 +111,6 @@ export type PartialUpdater = (
92
111
  mode?: UpdateMode,
93
112
  ) => Promise<void>;
94
113
 
95
- /**
96
- * Create a partial updater for fetching and applying RSC partial updates
97
- *
98
- * This function is shared between navigation-bridge and server-action-bridge
99
- * to handle partial RSC updates with HMR resilience.
100
- *
101
- * @param config - Partial update configuration
102
- * @returns fetchPartialUpdate function
103
- */
104
114
  export function createPartialUpdater(
105
115
  config: PartialUpdateConfig,
106
116
  ): PartialUpdater {
@@ -112,21 +122,12 @@ export function createPartialUpdater(
112
122
  getVersion = () => undefined,
113
123
  } = config;
114
124
 
115
- /**
116
- * Get current page's cached segments as an array
117
- */
118
125
  function getCurrentCachedSegments(): ResolvedSegment[] {
119
126
  const currentKey = store.getHistoryKey();
120
127
  const cached = store.getCachedSegments(currentKey);
121
128
  return cached?.segments || [];
122
129
  }
123
130
 
124
- /**
125
- * Fetch partial update and trigger UI update
126
- *
127
- * @param tx - Transaction for committing segment state (required)
128
- * @param signal - AbortSignal to check if navigation is stale (not for aborting fetch)
129
- */
130
131
  async function fetchPartialUpdate(
131
132
  targetUrl: string,
132
133
  segmentIds: string[] | undefined,
@@ -138,114 +139,114 @@ export function createPartialUpdater(
138
139
  const segmentState = store.getSegmentState();
139
140
  const url = targetUrl || window.location.href;
140
141
 
141
- // Capture history key at start for stale revalidation consistency check
142
142
  const historyKeyAtStart = store.getHistoryKey();
143
143
 
144
- // Derive interceptSourceUrl from modes that carry it
145
- const interceptSourceUrl =
146
- mode.type === "stale-revalidation" ||
147
- mode.type === "action" ||
148
- mode.type === "navigate"
149
- ? mode.interceptSourceUrl
150
- : undefined;
144
+ const interceptSourceUrl = mode.interceptSourceUrl;
151
145
 
152
- // When leaving intercept, filter out intercept-specific segments
153
146
  let segments: string[];
154
147
  if (mode.type === "leave-intercept") {
155
148
  const currentSegments = segmentIds ?? segmentState.currentSegmentIds;
156
149
  const currentCached = getCurrentCachedSegments();
157
150
  const interceptIds = new Set(
158
- currentCached
159
- .filter((s) => s.namespace?.startsWith("intercept:"))
160
- .map((s) => s.id),
151
+ currentCached.filter(isInterceptSegment).map((s) => s.id),
161
152
  );
162
153
  segments = currentSegments.filter((id) => !interceptIds.has(id));
163
- debugLog(
164
- `[Browser] Leaving intercept - filtered segments: ${segments.join(", ")}`,
165
- );
154
+ if (IS_BROWSER_DEBUG) {
155
+ debugLog(
156
+ `[Browser] Leaving intercept - filtered segments: ${segments.join(", ")}`,
157
+ );
158
+ }
166
159
  } else {
167
160
  segments = segmentIds ?? segmentState.currentSegmentIds;
168
161
  }
169
162
 
170
- // For intercept revalidation, use the intercept source URL as previousUrl.
171
- // For leave-intercept, tx.currentUrl captures window.location.href at tx
172
- // creation, which on popstate is already the destination URL and would
173
- // tell the server "from == to". segmentState.currentUrl still points at
174
- // the URL the cached segments render (the intercept URL), which is the
175
- // correct "from" for the server's diff computation.
176
163
  const previousUrl =
177
164
  mode.type === "leave-intercept"
178
165
  ? segmentState.currentUrl || tx.currentUrl
179
166
  : interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
180
167
 
181
- debugLog(`\n[Browser] >>> NAVIGATION`);
182
- debugLog(`[Browser] From: ${previousUrl}`);
183
- debugLog(`[Browser] To: ${url}`);
184
- debugLog(`[Browser] Segments to send: ${segments.join(", ")}`);
185
- if (interceptSourceUrl) {
186
- debugLog(`[Browser] Intercept context from: ${interceptSourceUrl}`);
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
+ }
187
176
  }
188
177
 
189
- // Get cached segments for merging with server diff.
190
- // When navigating with targetCacheSegments, use those for consistency.
191
- // Otherwise fall back to current page's segments (for same-route revalidation).
192
178
  const targetCache =
193
- mode.type === "navigate" ? mode.targetCacheSegments : undefined;
194
- const cachedSegs =
195
- targetCache && targetCache.length > 0
196
- ? targetCache
197
- : getCurrentCachedSegments();
198
- const cachedSegsSource =
199
- targetCache && targetCache.length > 0 ? "history-cache" : "current-page";
200
- debugLog(
201
- `[Browser] cachedSegs source: ${cachedSegsSource} (${cachedSegs.length} segments: ${cachedSegs.map((s) => s.id).join(", ")})`,
202
- );
203
-
204
- // Fetch partial payload (no abort signal - RSC doesn't support it well)
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
+ }
189
+
205
190
  let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
206
191
  fetchResult = await client.fetchPartial({
207
192
  targetUrl: url,
208
193
  segmentIds: segments,
209
194
  previousUrl,
210
- // Mark stale when explicitly requested OR when no segments are sent
211
- // (action redirect sends empty segments for a fresh render).
212
195
  staleRevalidation:
213
196
  mode.type === "stale-revalidation" || segments.length === 0,
214
197
  version: getVersion(),
215
198
  routerId: store.getRouterId?.(),
216
199
  });
217
- // Mark navigation as streaming (response received, now parsing RSC).
218
- // Called after fetchPartial so pendingUrl stays set during the network wait,
219
- // allowing useLinkStatus to show per-link pending indicators.
220
200
  const streamingToken = tx.startStreaming();
221
- const { payload, streamComplete: rawStreamComplete } = fetchResult;
201
+ const {
202
+ payload,
203
+ streamComplete: rawStreamComplete,
204
+ fullyPrefetched,
205
+ } = fetchResult;
222
206
  debugLog("payload.metadata", payload.metadata);
223
207
 
224
- const streamComplete = rawStreamComplete.then(() => {
225
- streamingToken.end();
226
- });
227
-
228
- // Detect app switch: if routerId changed, the navigation crossed into
229
- // a different router (e.g., via host router path mount). Downgrade
230
- // partial to full so the entire tree is replaced without reconciliation
231
- // against stale segments from the previous app.
232
- if (payload.metadata?.routerId) {
233
- const prevRouterId = store.getRouterId?.();
234
- if (prevRouterId && prevRouterId !== payload.metadata.routerId) {
235
- debugLog(
236
- `[Browser] App switch detected (${prevRouterId} ${payload.metadata.routerId}), forcing full update`,
237
- );
238
- payload.metadata.isPartial = false;
239
- }
240
- store.setRouterId?.(payload.metadata.routerId);
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;
241
225
  }
242
226
 
243
- // Handle server-side redirect with state
244
227
  if (payload.metadata?.redirect) {
245
228
  if (signal?.aborted) {
246
229
  debugLog("[Browser] Ignoring stale redirect (aborted)");
247
230
  return;
248
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
+ }
249
250
  const redirectUrl = validateRedirectOrigin(
250
251
  payload.metadata.redirect.url,
251
252
  window.location.origin,
@@ -267,10 +268,11 @@ export function createPartialUpdater(
267
268
  return;
268
269
  }
269
270
 
270
- debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
271
- 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
+ }
272
275
 
273
- // If diff is empty, nothing changed on server side.
274
276
  if (!diff || diff.length === 0) {
275
277
  const matchedIds = matched || [];
276
278
  const cacheMap = new Map(cachedSegs.map((s) => [s.id, s]));
@@ -278,8 +280,7 @@ export function createPartialUpdater(
278
280
  .map((id: string) => cacheMap.get(id))
279
281
  .filter(Boolean) as ResolvedSegment[];
280
282
 
281
- // When navigating with cached segments to a different route, render them.
282
- if (mode.type === "navigate" && targetCache && targetCache.length > 0) {
283
+ if (mode.type === "navigate" && targetCache) {
283
284
  debugLog(
284
285
  "[Browser] No diff but navigating with cached segments - rendering target route",
285
286
  );
@@ -293,10 +294,6 @@ export function createPartialUpdater(
293
294
  existingSegments,
294
295
  );
295
296
 
296
- // tx.commit() cached the source page's handleData because
297
- // eventController hasn't been updated yet. Overwrite with the
298
- // correct cached handleData to prevent cache corruption on
299
- // subsequent navigations to this same URL.
300
297
  if (mode.targetCacheHandleData) {
301
298
  store.updateCacheHandleData(
302
299
  store.getHistoryKey(),
@@ -304,10 +301,6 @@ export function createPartialUpdater(
304
301
  );
305
302
  }
306
303
 
307
- // Include cachedHandleData in metadata so NavigationProvider can restore
308
- // breadcrumbs and other handle data from cache.
309
- // Remove `handles` from metadata to prevent NavigationProvider from
310
- // processing an empty handles stream, which would clear the cached breadcrumbs.
311
304
  const { handles: _unusedHandles, ...metadataWithoutHandles } =
312
305
  payload.metadata!;
313
306
  const cachedUpdate = {
@@ -319,10 +312,7 @@ export function createPartialUpdater(
319
312
  scroll: toScrollPayload(commitScroll),
320
313
  };
321
314
 
322
- const cachedHasTransition = existingSegments.some(
323
- (s) => s.transition,
324
- );
325
- if (cachedHasTransition) {
315
+ if (shouldStartViewTransition(existingSegments)) {
326
316
  startTransition(() => {
327
317
  if (addTransitionType) {
328
318
  addTransitionType("navigation");
@@ -337,7 +327,6 @@ export function createPartialUpdater(
337
327
  return;
338
328
  }
339
329
 
340
- // When leaving intercept, force re-render even with empty diff
341
330
  if (mode.type === "leave-intercept") {
342
331
  debugLog(
343
332
  "[Browser] Leaving intercept - forcing re-render to remove modal",
@@ -362,7 +351,6 @@ export function createPartialUpdater(
362
351
  return;
363
352
  }
364
353
 
365
- // Same route revalidation with no changes - skip UI update
366
354
  debugLog(
367
355
  "[Browser] No changes - all revalidations returned false, keeping existing UI",
368
356
  );
@@ -371,7 +359,6 @@ export function createPartialUpdater(
371
359
  return;
372
360
  }
373
361
 
374
- // Reconcile server segments with cached segments (single source of truth)
375
362
  const matchedIds = matched || [];
376
363
  const actor: ReconcileActor =
377
364
  mode.type === "stale-revalidation" || mode.type === "action"
@@ -387,7 +374,6 @@ export function createPartialUpdater(
387
374
  insertMissingDiff: true,
388
375
  });
389
376
 
390
- // HMR RESILIENCE: Check if we're missing any matched segments
391
377
  const reconciledIdSet = new Set(reconciled.segments.map((s) => s.id));
392
378
  const missingIds = matchedIds.filter(
393
379
  (id: string) => !reconciledIdSet.has(id),
@@ -409,13 +395,19 @@ export function createPartialUpdater(
409
395
  return;
410
396
  }
411
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
+ );
412
405
  return;
413
406
  }
414
407
  console.warn(
415
408
  `[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
416
409
  );
417
410
 
418
- // Refetch with empty segments = server sends everything
419
411
  return fetchPartialUpdate(url, [], true, signal, tx, mode);
420
412
  }
421
413
 
@@ -424,44 +416,59 @@ export function createPartialUpdater(
424
416
  return;
425
417
  }
426
418
 
427
- // Rebuild tree on client (await for loader data resolution)
428
419
  const renderOptions = {
429
420
  isAction: mode.type === "action",
430
- 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,
431
429
  interceptSegments:
432
430
  reconciled.interceptSegments.length > 0
433
431
  ? reconciled.interceptSegments
434
432
  : undefined,
435
433
  };
436
- const newTree = await (signal
437
- ? 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([
438
453
  renderSegments(reconciled.mainSegments, renderOptions),
439
- new Promise<never>((_, reject) => {
440
- if (signal.aborted) {
441
- reject(new DOMException("Navigation aborted", "AbortError"));
442
- }
443
- signal.addEventListener("abort", () => {
444
- reject(new DOMException("Navigation aborted", "AbortError"));
445
- });
446
- }),
447
- ])
448
- : renderSegments(reconciled.mainSegments, renderOptions));
449
-
450
- // 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
+
451
463
  if (signal?.aborted) {
452
464
  debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
453
465
  return;
454
466
  }
455
467
 
456
- // Check if this is an intercept response (any slot is active)
457
468
  const isInterceptResponse = hasActiveInterceptSlots(
458
469
  payload.metadata?.slots,
459
470
  );
460
471
 
461
- // Track intercept context (only on navigation, not actions or stale revalidation)
462
- // Use the authoritative source from mode/history state when restoring an
463
- // intercept via popstate cache miss; fall back to the current URL for fresh
464
- // intercept navigations.
465
472
  const effectiveInterceptSource =
466
473
  interceptSourceUrl || segmentState.currentUrl;
467
474
  if (mode.type !== "action" && mode.type !== "stale-revalidation") {
@@ -472,9 +479,6 @@ export function createPartialUpdater(
472
479
  }
473
480
  }
474
481
 
475
- // Commit navigation - use server's matched as the authoritative segment ID list.
476
- // reconciled.segments may be missing IDs (e.g., loader segments not in diff or cache)
477
- // but the server's matched always includes all expected segment IDs.
478
482
  const allSegmentIds = matchedIds;
479
483
  const serverLocationState = payload.metadata?.locationState;
480
484
  const overrides: CommitOverrides | undefined = isInterceptResponse
@@ -493,7 +497,6 @@ export function createPartialUpdater(
493
497
  overrides,
494
498
  );
495
499
 
496
- // For stale revalidation: verify history key hasn't changed before updating UI
497
500
  if (mode.type === "stale-revalidation") {
498
501
  const historyKeyNow = store.getHistoryKey();
499
502
  if (historyKeyNow !== historyKeyAtStart) {
@@ -506,9 +509,22 @@ export function createPartialUpdater(
506
509
 
507
510
  debugLog("[partial-update] updating document");
508
511
 
509
- // Emit update to trigger React render.
510
- // Scroll info is included so NavigationProvider applies it after React commits.
511
- 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
+ }
512
528
  const scrollPayload = toScrollPayload(navScroll);
513
529
 
514
530
  if (mode.type === "action" || mode.type === "stale-revalidation") {
@@ -534,6 +550,20 @@ export function createPartialUpdater(
534
550
  });
535
551
  });
536
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).
537
567
  onUpdate({
538
568
  root: newTree,
539
569
  metadata: payload.metadata!,
@@ -544,7 +574,6 @@ export function createPartialUpdater(
544
574
  debugLog("[Browser] Navigation complete");
545
575
  return;
546
576
  } else {
547
- // Full update (fallback)
548
577
  console.warn(`[Browser] Full update (fallback)`);
549
578
 
550
579
  const segments = payload.metadata?.segments || [];
@@ -570,13 +599,23 @@ export function createPartialUpdater(
570
599
  })
571
600
  : tx.commit(segmentIds, segments);
572
601
 
573
- const fullHasTransition = segments.some(
574
- (s: ResolvedSegment) => s.transition,
575
- );
602
+ const fullHasTransition = shouldStartViewTransition(segments);
576
603
  const fullScrollPayload = toScrollPayload(fullScroll);
577
604
 
578
605
  if (mode.type === "stale-revalidation") {
579
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
+ }
580
619
  startTransition(() => {
581
620
  if (fullHasTransition && addTransitionType) {
582
621
  addTransitionType("action");
@@ -588,7 +627,7 @@ export function createPartialUpdater(
588
627
  });
589
628
  });
590
629
  } else if (mode.type === "action") {
591
- startTransition(async () => {
630
+ startTransition(() => {
592
631
  if (fullHasTransition && addTransitionType) {
593
632
  addTransitionType("action");
594
633
  }