@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.140

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