@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17

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