@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1c0bdfad

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 (400) hide show
  1. package/AGENTS.md +12 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +441 -134
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3453 -1240
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +76 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +64 -2
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +247 -23
  12. package/skills/caching/SKILL.md +318 -15
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/debug-manifest/SKILL.md +4 -2
  16. package/skills/document-cache/SKILL.md +78 -55
  17. package/skills/handler-use/SKILL.md +364 -0
  18. package/skills/hooks/SKILL.md +250 -30
  19. package/skills/host-router/SKILL.md +83 -23
  20. package/skills/i18n/SKILL.md +276 -0
  21. package/skills/intercept/SKILL.md +50 -6
  22. package/skills/layout/SKILL.md +35 -9
  23. package/skills/links/SKILL.md +249 -17
  24. package/skills/loader/SKILL.md +279 -53
  25. package/skills/middleware/SKILL.md +52 -13
  26. package/skills/migrate-nextjs/SKILL.md +584 -0
  27. package/skills/migrate-react-router/SKILL.md +771 -0
  28. package/skills/mime-routes/SKILL.md +28 -1
  29. package/skills/observability/SKILL.md +172 -0
  30. package/skills/parallel/SKILL.md +203 -7
  31. package/skills/prerender/SKILL.md +153 -109
  32. package/skills/rango/SKILL.md +251 -22
  33. package/skills/react-compiler/SKILL.md +168 -0
  34. package/skills/response-routes/SKILL.md +123 -48
  35. package/skills/route/SKILL.md +101 -5
  36. package/skills/router-setup/SKILL.md +116 -8
  37. package/skills/scripts/SKILL.md +179 -0
  38. package/skills/server-actions/SKILL.md +775 -0
  39. package/skills/streams-and-websockets/SKILL.md +283 -0
  40. package/skills/tailwind/SKILL.md +27 -3
  41. package/skills/testing/SKILL.md +129 -0
  42. package/skills/testing/bindings.md +103 -0
  43. package/skills/testing/cache-prerender.md +127 -0
  44. package/skills/testing/client-components.md +124 -0
  45. package/skills/testing/e2e-parity.md +125 -0
  46. package/skills/testing/flight.md +91 -0
  47. package/skills/testing/handles.md +129 -0
  48. package/skills/testing/loader.md +128 -0
  49. package/skills/testing/middleware.md +99 -0
  50. package/skills/testing/render-handler.md +121 -0
  51. package/skills/testing/response-routes.md +95 -0
  52. package/skills/testing/reverse-and-types.md +84 -0
  53. package/skills/testing/server-actions.md +107 -0
  54. package/skills/testing/server-tree.md +128 -0
  55. package/skills/testing/setup.md +123 -0
  56. package/skills/typesafety/SKILL.md +332 -29
  57. package/skills/use-cache/SKILL.md +46 -14
  58. package/skills/view-transitions/SKILL.md +294 -0
  59. package/src/__augment-tests__/augment.ts +81 -0
  60. package/src/__augment-tests__/augmented.check.ts +116 -0
  61. package/src/__internal.ts +1 -66
  62. package/src/browser/action-coordinator.ts +53 -36
  63. package/src/browser/action-fence.ts +47 -0
  64. package/src/browser/app-shell.ts +39 -0
  65. package/src/browser/app-version.ts +14 -0
  66. package/src/browser/connection-warmup.ts +134 -0
  67. package/src/browser/cookie-name.ts +140 -0
  68. package/src/browser/event-controller.ts +197 -150
  69. package/src/browser/history-state.ts +21 -0
  70. package/src/browser/index.ts +3 -3
  71. package/src/browser/invalidate-client-cache.ts +52 -0
  72. package/src/browser/navigation-bridge.ts +111 -31
  73. package/src/browser/navigation-client.ts +201 -67
  74. package/src/browser/navigation-store-handle.ts +38 -0
  75. package/src/browser/navigation-store.ts +76 -67
  76. package/src/browser/navigation-transaction.ts +18 -66
  77. package/src/browser/network-error-handler.ts +34 -7
  78. package/src/browser/partial-update.ts +187 -112
  79. package/src/browser/prefetch/cache.ts +230 -35
  80. package/src/browser/prefetch/fetch.ts +338 -39
  81. package/src/browser/prefetch/queue.ts +126 -20
  82. package/src/browser/prefetch/resource-ready.ts +77 -0
  83. package/src/browser/rango-state.ts +158 -76
  84. package/src/browser/react/Link.tsx +111 -16
  85. package/src/browser/react/NavigationProvider.tsx +135 -120
  86. package/src/browser/react/ScrollRestoration.tsx +10 -6
  87. package/src/browser/react/context.ts +7 -2
  88. package/src/browser/react/filter-segment-order.ts +66 -7
  89. package/src/browser/react/index.ts +0 -48
  90. package/src/browser/react/location-state-shared.ts +178 -8
  91. package/src/browser/react/location-state.ts +39 -14
  92. package/src/browser/react/use-action.ts +6 -15
  93. package/src/browser/react/use-handle.ts +23 -69
  94. package/src/browser/react/use-href.tsx +8 -1
  95. package/src/browser/react/use-link-status.ts +33 -8
  96. package/src/browser/react/use-navigation.ts +32 -7
  97. package/src/browser/react/use-params.ts +20 -10
  98. package/src/browser/react/use-reverse.ts +106 -0
  99. package/src/browser/react/use-router.ts +46 -11
  100. package/src/browser/react/use-search-params.ts +0 -5
  101. package/src/browser/react/use-segments.ts +11 -21
  102. package/src/browser/response-adapter.ts +80 -5
  103. package/src/browser/rsc-router.tsx +218 -76
  104. package/src/browser/scroll-restoration.ts +54 -42
  105. package/src/browser/segment-reconciler.ts +36 -9
  106. package/src/browser/segment-structure-assert.ts +2 -2
  107. package/src/browser/server-action-bridge.ts +222 -61
  108. package/src/browser/types.ts +91 -11
  109. package/src/browser/validate-redirect-origin.ts +43 -16
  110. package/src/build/collect-fallback-refs.ts +107 -0
  111. package/src/build/generate-manifest.ts +65 -40
  112. package/src/build/generate-route-types.ts +5 -1
  113. package/src/build/index.ts +8 -2
  114. package/src/build/prefix-tree-utils.ts +123 -0
  115. package/src/build/route-trie.ts +165 -36
  116. package/src/build/route-types/ast-route-extraction.ts +15 -8
  117. package/src/build/route-types/codegen.ts +16 -5
  118. package/src/build/route-types/include-resolution.ts +125 -24
  119. package/src/build/route-types/param-extraction.ts +6 -3
  120. package/src/build/route-types/per-module-writer.ts +22 -6
  121. package/src/build/route-types/router-processing.ts +272 -96
  122. package/src/build/route-types/scan-filter.ts +9 -2
  123. package/src/build/route-types/source-scan.ts +216 -0
  124. package/src/build/runtime-discovery.ts +9 -20
  125. package/src/cache/cache-error.ts +104 -0
  126. package/src/cache/cache-key-utils.ts +29 -13
  127. package/src/cache/cache-policy.ts +108 -34
  128. package/src/cache/cache-runtime.ts +214 -48
  129. package/src/cache/cache-scope.ts +236 -89
  130. package/src/cache/cache-tag.ts +103 -0
  131. package/src/cache/cf/cf-base64.ts +33 -0
  132. package/src/cache/cf/cf-cache-constants.ts +127 -0
  133. package/src/cache/cf/cf-cache-store.ts +2224 -171
  134. package/src/cache/cf/cf-cache-types.ts +349 -0
  135. package/src/cache/cf/cf-kv-utils.ts +46 -0
  136. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  137. package/src/cache/cf/index.ts +11 -17
  138. package/src/cache/document-cache.ts +89 -27
  139. package/src/cache/handle-snapshot.ts +70 -0
  140. package/src/cache/index.ts +11 -20
  141. package/src/cache/memory-segment-store.ts +136 -37
  142. package/src/cache/profile-registry.ts +31 -31
  143. package/src/cache/read-through-swr.ts +41 -11
  144. package/src/cache/segment-codec.ts +9 -17
  145. package/src/cache/tag-invalidation.ts +230 -0
  146. package/src/cache/taint.ts +55 -0
  147. package/src/cache/types.ts +37 -100
  148. package/src/client.rsc.tsx +44 -21
  149. package/src/client.tsx +119 -290
  150. package/src/cloudflare/index.ts +11 -0
  151. package/src/cloudflare/tracing.ts +109 -0
  152. package/src/component-utils.ts +19 -0
  153. package/src/components/DefaultDocument.tsx +8 -2
  154. package/src/context-var.ts +84 -2
  155. package/src/debug.ts +2 -2
  156. package/src/decode-loader-results.ts +52 -0
  157. package/src/defer.ts +196 -0
  158. package/src/deps/ssr.ts +0 -1
  159. package/src/encode-kv.ts +49 -0
  160. package/src/errors.ts +30 -4
  161. package/src/escape-script.ts +52 -0
  162. package/src/handle.ts +70 -22
  163. package/src/handles/MetaTags.tsx +56 -19
  164. package/src/handles/Scripts.tsx +183 -0
  165. package/src/handles/breadcrumbs.ts +37 -8
  166. package/src/handles/is-thenable.ts +19 -0
  167. package/src/handles/meta.ts +51 -40
  168. package/src/handles/script.ts +244 -0
  169. package/src/host/cookie-handler.ts +9 -60
  170. package/src/host/errors.ts +0 -24
  171. package/src/host/index.ts +8 -2
  172. package/src/host/pattern-matcher.ts +23 -52
  173. package/src/host/router.ts +107 -99
  174. package/src/host/testing.ts +40 -27
  175. package/src/host/types.ts +37 -4
  176. package/src/host/utils.ts +1 -1
  177. package/src/href-client.ts +137 -22
  178. package/src/index.rsc.ts +93 -12
  179. package/src/index.ts +133 -15
  180. package/src/internal-debug.ts +11 -10
  181. package/src/loader-store.ts +500 -0
  182. package/src/loader.rsc.ts +20 -13
  183. package/src/loader.ts +12 -11
  184. package/src/missing-id-error.ts +68 -0
  185. package/src/outlet-context.ts +1 -1
  186. package/src/outlet-provider.tsx +1 -5
  187. package/src/prerender/param-hash.ts +16 -16
  188. package/src/prerender/store.ts +37 -41
  189. package/src/prerender.ts +198 -82
  190. package/src/redirect-origin.ts +100 -0
  191. package/src/regex-escape.ts +8 -0
  192. package/src/render-error-thrower.tsx +20 -0
  193. package/src/response-utils.ts +62 -0
  194. package/src/reverse.ts +65 -15
  195. package/src/root-error-boundary.tsx +1 -19
  196. package/src/route-content-wrapper.tsx +7 -72
  197. package/src/route-definition/dsl-helpers.ts +469 -276
  198. package/src/route-definition/helper-factories.ts +29 -139
  199. package/src/route-definition/helpers-types.ts +113 -37
  200. package/src/route-definition/index.ts +3 -0
  201. package/src/route-definition/redirect.ts +53 -12
  202. package/src/route-definition/resolve-handler-use.ts +161 -0
  203. package/src/route-definition/use-item-types.ts +32 -0
  204. package/src/route-map-builder.ts +7 -17
  205. package/src/route-types.ts +37 -41
  206. package/src/router/basename.ts +14 -0
  207. package/src/router/content-negotiation.ts +164 -17
  208. package/src/router/error-handling.ts +45 -18
  209. package/src/router/find-match.ts +45 -22
  210. package/src/router/handler-context.ts +83 -39
  211. package/src/router/instrument.ts +350 -0
  212. package/src/router/intercept-resolution.ts +50 -24
  213. package/src/router/lazy-includes.ts +19 -53
  214. package/src/router/loader-resolution.ts +274 -56
  215. package/src/router/logging.ts +5 -8
  216. package/src/router/manifest.ts +49 -45
  217. package/src/router/match-api.ts +120 -204
  218. package/src/router/match-context.ts +0 -22
  219. package/src/router/match-handlers.ts +58 -58
  220. package/src/router/match-middleware/background-revalidation.ts +33 -6
  221. package/src/router/match-middleware/cache-lookup.ts +214 -263
  222. package/src/router/match-middleware/cache-store.ts +73 -33
  223. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  224. package/src/router/match-middleware/segment-resolution.ts +52 -18
  225. package/src/router/match-pipelines.ts +1 -42
  226. package/src/router/match-result.ts +104 -40
  227. package/src/router/metrics.ts +5 -34
  228. package/src/router/middleware-types.ts +13 -142
  229. package/src/router/middleware.ts +270 -172
  230. package/src/router/navigation-snapshot.ts +131 -0
  231. package/src/router/params-util.ts +23 -0
  232. package/src/router/pattern-matching.ts +132 -90
  233. package/src/router/prefetch-cache-ttl.ts +51 -0
  234. package/src/router/prerender-match.ts +195 -56
  235. package/src/router/preview-match.ts +32 -102
  236. package/src/router/request-classification.ts +276 -0
  237. package/src/router/revalidation.ts +123 -73
  238. package/src/router/route-snapshot.ts +244 -0
  239. package/src/router/router-context.ts +8 -28
  240. package/src/router/router-interfaces.ts +115 -35
  241. package/src/router/router-options.ts +172 -15
  242. package/src/router/router-registry.ts +2 -5
  243. package/src/router/segment-resolution/fresh.ts +264 -77
  244. package/src/router/segment-resolution/helpers.ts +115 -30
  245. package/src/router/segment-resolution/loader-cache.ts +63 -37
  246. package/src/router/segment-resolution/revalidation.ts +474 -385
  247. package/src/router/segment-resolution/static-store.ts +19 -5
  248. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  249. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  250. package/src/router/segment-resolution.ts +5 -1
  251. package/src/router/segment-wrappers.ts +8 -5
  252. package/src/router/state-cookie-name.ts +33 -0
  253. package/src/router/substitute-pattern-params.ts +56 -0
  254. package/src/router/telemetry-otel.ts +161 -199
  255. package/src/router/telemetry.ts +96 -19
  256. package/src/router/timeout.ts +0 -20
  257. package/src/router/tracing.ts +206 -0
  258. package/src/router/trie-matching.ts +163 -59
  259. package/src/router/types.ts +10 -63
  260. package/src/router/url-params.ts +44 -0
  261. package/src/router.ts +163 -55
  262. package/src/rsc/handler-context.ts +3 -2
  263. package/src/rsc/handler.ts +658 -511
  264. package/src/rsc/helpers.ts +168 -46
  265. package/src/rsc/index.ts +2 -5
  266. package/src/rsc/json-route-result.ts +38 -0
  267. package/src/rsc/loader-fetch.ts +127 -31
  268. package/src/rsc/manifest-init.ts +33 -42
  269. package/src/rsc/origin-guard.ts +39 -25
  270. package/src/rsc/progressive-enhancement.ts +77 -11
  271. package/src/rsc/redirect-guard.ts +99 -0
  272. package/src/rsc/response-cache-serve.ts +238 -0
  273. package/src/rsc/response-error.ts +79 -12
  274. package/src/rsc/response-route-handler.ts +99 -189
  275. package/src/rsc/rsc-rendering.ts +105 -72
  276. package/src/rsc/runtime-warnings.ts +23 -10
  277. package/src/rsc/server-action.ts +263 -112
  278. package/src/rsc/ssr-setup.ts +18 -2
  279. package/src/rsc/types.ts +32 -6
  280. package/src/runtime-env.ts +18 -0
  281. package/src/search-params.ts +35 -30
  282. package/src/segment-content-promise.ts +67 -0
  283. package/src/segment-loader-promise.ts +149 -0
  284. package/src/segment-system.tsx +281 -129
  285. package/src/serialize.ts +243 -0
  286. package/src/server/context.ts +309 -61
  287. package/src/server/cookie-parse.ts +32 -0
  288. package/src/server/cookie-store.ts +80 -5
  289. package/src/server/handle-store.ts +40 -38
  290. package/src/server/loader-registry.ts +26 -46
  291. package/src/server/request-context.ts +398 -172
  292. package/src/ssr/index.tsx +25 -16
  293. package/src/static-handler.ts +27 -18
  294. package/src/testing/cache-status.ts +162 -0
  295. package/src/testing/collect-handle.ts +40 -0
  296. package/src/testing/dispatch.ts +701 -0
  297. package/src/testing/dom.entry.ts +22 -0
  298. package/src/testing/e2e/fixture.ts +188 -0
  299. package/src/testing/e2e/index.ts +128 -0
  300. package/src/testing/e2e/matchers.ts +35 -0
  301. package/src/testing/e2e/page-helpers.ts +272 -0
  302. package/src/testing/e2e/parity.ts +387 -0
  303. package/src/testing/e2e/server.ts +195 -0
  304. package/src/testing/flight-matchers.ts +97 -0
  305. package/src/testing/flight-normalize.ts +11 -0
  306. package/src/testing/flight-runtime.d.ts +57 -0
  307. package/src/testing/flight-tree.ts +682 -0
  308. package/src/testing/flight.entry.ts +52 -0
  309. package/src/testing/flight.ts +257 -0
  310. package/src/testing/generated-routes.ts +183 -0
  311. package/src/testing/index.ts +99 -0
  312. package/src/testing/internal/context.ts +371 -0
  313. package/src/testing/internal/flight-client-globals.ts +30 -0
  314. package/src/testing/internal/seed-vars.ts +54 -0
  315. package/src/testing/render-handler.ts +343 -0
  316. package/src/testing/render-route.tsx +581 -0
  317. package/src/testing/run-loader.ts +385 -0
  318. package/src/testing/run-middleware.ts +205 -0
  319. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  320. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  321. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  322. package/src/testing/vitest-stubs/version.ts +5 -0
  323. package/src/testing/vitest.ts +305 -0
  324. package/src/theme/ThemeProvider.tsx +20 -58
  325. package/src/theme/ThemeScript.tsx +7 -9
  326. package/src/theme/constants.ts +52 -13
  327. package/src/theme/index.ts +0 -7
  328. package/src/theme/theme-context.ts +1 -5
  329. package/src/theme/theme-script.ts +22 -21
  330. package/src/theme/use-theme.ts +0 -3
  331. package/src/types/boundaries.ts +0 -35
  332. package/src/types/cache-types.ts +17 -8
  333. package/src/types/error-types.ts +30 -90
  334. package/src/types/global-namespace.ts +54 -41
  335. package/src/types/handler-context.ts +233 -81
  336. package/src/types/index.ts +1 -10
  337. package/src/types/loader-types.ts +44 -15
  338. package/src/types/request-scope.ts +112 -0
  339. package/src/types/route-config.ts +6 -50
  340. package/src/types/route-entry.ts +19 -7
  341. package/src/types/segments.ts +37 -14
  342. package/src/urls/include-helper.ts +33 -70
  343. package/src/urls/index.ts +1 -11
  344. package/src/urls/path-helper-types.ts +58 -11
  345. package/src/urls/path-helper.ts +57 -111
  346. package/src/urls/pattern-types.ts +48 -19
  347. package/src/urls/response-types.ts +25 -22
  348. package/src/urls/type-extraction.ts +58 -139
  349. package/src/urls/urls-function.ts +1 -18
  350. package/src/use-loader.tsx +346 -89
  351. package/src/vite/debug.ts +185 -0
  352. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  353. package/src/vite/discovery/discover-routers.ts +130 -85
  354. package/src/vite/discovery/discovery-errors.ts +194 -0
  355. package/src/vite/discovery/gate-state.ts +171 -0
  356. package/src/vite/discovery/prerender-collection.ts +214 -132
  357. package/src/vite/discovery/route-types-writer.ts +40 -84
  358. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  359. package/src/vite/discovery/state.ts +57 -6
  360. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  361. package/src/vite/index.ts +6 -0
  362. package/src/vite/inject-client-debug.ts +36 -0
  363. package/src/vite/plugin-types.ts +155 -65
  364. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  365. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  366. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  367. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  368. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  369. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  370. package/src/vite/plugins/expose-action-id.ts +49 -98
  371. package/src/vite/plugins/expose-id-utils.ts +96 -51
  372. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  373. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  374. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  375. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  376. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  377. package/src/vite/plugins/performance-tracks.ts +89 -0
  378. package/src/vite/plugins/refresh-cmd.ts +89 -27
  379. package/src/vite/plugins/use-cache-transform.ts +73 -83
  380. package/src/vite/plugins/version-injector.ts +21 -25
  381. package/src/vite/plugins/version-plugin.ts +46 -37
  382. package/src/vite/plugins/virtual-entries.ts +13 -18
  383. package/src/vite/rango.ts +238 -295
  384. package/src/vite/router-discovery.ts +940 -149
  385. package/src/vite/utils/ast-handler-extract.ts +26 -35
  386. package/src/vite/utils/banner.ts +4 -4
  387. package/src/vite/utils/bundle-analysis.ts +10 -15
  388. package/src/vite/utils/client-chunks.ts +184 -0
  389. package/src/vite/utils/directive-prologue.ts +40 -0
  390. package/src/vite/utils/forward-user-plugins.ts +171 -0
  391. package/src/vite/utils/manifest-utils.ts +4 -59
  392. package/src/vite/utils/package-resolution.ts +20 -52
  393. package/src/vite/utils/prerender-utils.ts +81 -34
  394. package/src/vite/utils/shared-utils.ts +92 -42
  395. package/src/browser/action-response-classifier.ts +0 -99
  396. package/src/browser/react/use-client-cache.ts +0 -58
  397. package/src/browser/shallow.ts +0 -40
  398. package/src/handles/index.ts +0 -7
  399. package/src/network-error-thrower.tsx +0 -23
  400. 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,45 +167,76 @@ 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
- const { payload, streamComplete: rawStreamComplete } = fetchResult;
191
+ const {
192
+ payload,
193
+ streamComplete: rawStreamComplete,
194
+ fullyPrefetched,
195
+ } = fetchResult;
195
196
  debugLog("payload.metadata", payload.metadata);
196
197
 
197
- const streamComplete = rawStreamComplete.then(() => {
198
- streamingToken.end();
199
- });
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
+ }
200
216
 
201
- // Handle server-side redirect with state
202
217
  if (payload.metadata?.redirect) {
203
218
  if (signal?.aborted) {
204
219
  debugLog("[Browser] Ignoring stale redirect (aborted)");
205
220
  return;
206
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
+ }
207
240
  const redirectUrl = validateRedirectOrigin(
208
241
  payload.metadata.redirect.url,
209
242
  window.location.origin,
@@ -228,7 +261,6 @@ export function createPartialUpdater(
228
261
  debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
229
262
  debugLog(`[Browser] Diff: ${diff?.join(", ")}`);
230
263
 
231
- // If diff is empty, nothing changed on server side.
232
264
  if (!diff || diff.length === 0) {
233
265
  const matchedIds = matched || [];
234
266
  const cacheMap = new Map(cachedSegs.map((s) => [s.id, s]));
@@ -236,8 +268,7 @@ export function createPartialUpdater(
236
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
- if (mode.type === "navigate" && targetCache && targetCache.length > 0) {
271
+ if (mode.type === "navigate" && targetCache) {
241
272
  debugLog(
242
273
  "[Browser] No diff but navigating with cached segments - rendering target route",
243
274
  );
@@ -246,12 +277,18 @@ export function createPartialUpdater(
246
277
  forceAwait: true,
247
278
  });
248
279
 
249
- 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
+ }
250
291
 
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
292
  const { handles: _unusedHandles, ...metadataWithoutHandles } =
256
293
  payload.metadata!;
257
294
  const cachedUpdate = {
@@ -260,12 +297,10 @@ export function createPartialUpdater(
260
297
  ...metadataWithoutHandles,
261
298
  cachedHandleData: mode.targetCacheHandleData,
262
299
  },
300
+ scroll: toScrollPayload(commitScroll),
263
301
  };
264
302
 
265
- const cachedHasTransition = existingSegments.some(
266
- (s) => s.transition,
267
- );
268
- if (cachedHasTransition) {
303
+ if (shouldStartViewTransition(existingSegments)) {
269
304
  startTransition(() => {
270
305
  if (addTransitionType) {
271
306
  addTransitionType("navigation");
@@ -280,7 +315,6 @@ export function createPartialUpdater(
280
315
  return;
281
316
  }
282
317
 
283
- // When leaving intercept, force re-render even with empty diff
284
318
  if (mode.type === "leave-intercept") {
285
319
  debugLog(
286
320
  "[Browser] Leaving intercept - forcing re-render to remove modal",
@@ -290,18 +324,21 @@ export function createPartialUpdater(
290
324
  forceAwait: true,
291
325
  });
292
326
 
293
- tx.commit(matchedIds, existingSegments);
327
+ const { scroll: leaveScroll } = tx.commit(
328
+ matchedIds,
329
+ existingSegments,
330
+ );
294
331
 
295
332
  onUpdate({
296
333
  root: newTree,
297
334
  metadata: payload.metadata,
335
+ scroll: toScrollPayload(leaveScroll),
298
336
  });
299
337
 
300
338
  debugLog("[Browser] Navigation complete (left intercept)");
301
339
  return;
302
340
  }
303
341
 
304
- // Same route revalidation with no changes - skip UI update
305
342
  debugLog(
306
343
  "[Browser] No changes - all revalidations returned false, keeping existing UI",
307
344
  );
@@ -310,7 +347,6 @@ export function createPartialUpdater(
310
347
  return;
311
348
  }
312
349
 
313
- // Reconcile server segments with cached segments (single source of truth)
314
350
  const matchedIds = matched || [];
315
351
  const actor: ReconcileActor =
316
352
  mode.type === "stale-revalidation" || mode.type === "action"
@@ -326,7 +362,6 @@ export function createPartialUpdater(
326
362
  insertMissingDiff: true,
327
363
  });
328
364
 
329
- // HMR RESILIENCE: Check if we're missing any matched segments
330
365
  const reconciledIdSet = new Set(reconciled.segments.map((s) => s.id));
331
366
  const missingIds = matchedIds.filter(
332
367
  (id: string) => !reconciledIdSet.has(id),
@@ -354,7 +389,6 @@ export function createPartialUpdater(
354
389
  `[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
355
390
  );
356
391
 
357
- // Refetch with empty segments = server sends everything
358
392
  return fetchPartialUpdate(url, [], true, signal, tx, mode);
359
393
  }
360
394
 
@@ -363,7 +397,6 @@ export function createPartialUpdater(
363
397
  return;
364
398
  }
365
399
 
366
- // Rebuild tree on client (await for loader data resolution)
367
400
  const renderOptions = {
368
401
  isAction: mode.type === "action",
369
402
  forceAwait: mode.type === "stale-revalidation",
@@ -372,35 +405,44 @@ export function createPartialUpdater(
372
405
  ? reconciled.interceptSegments
373
406
  : undefined,
374
407
  };
375
- const newTree = await (signal
376
- ? Promise.race([
408
+ let newTree: Awaited<ReturnType<typeof renderSegments>>;
409
+ if (signal) {
410
+ // Race render against abort. Store the abort handler and register it
411
+ // { once:true } so a non-aborted render (which wins the race) can
412
+ // remove it in finally — otherwise the listener stays attached and the
413
+ // rejecting promise never settles. Mirrors teeWithCompletion in
414
+ // browser/response-adapter.ts.
415
+ let onAbort: (() => void) | undefined;
416
+ const abortPromise = new Promise<never>((_, reject) => {
417
+ if (signal.aborted) {
418
+ reject(new DOMException("Navigation aborted", "AbortError"));
419
+ return;
420
+ }
421
+ onAbort = () =>
422
+ reject(new DOMException("Navigation aborted", "AbortError"));
423
+ signal.addEventListener("abort", onAbort, { once: true });
424
+ });
425
+ try {
426
+ newTree = await Promise.race([
377
427
  renderSegments(reconciled.mainSegments, renderOptions),
378
- new Promise<never>((_, reject) => {
379
- if (signal.aborted) {
380
- reject(new DOMException("Navigation aborted", "AbortError"));
381
- }
382
- signal.addEventListener("abort", () => {
383
- reject(new DOMException("Navigation aborted", "AbortError"));
384
- });
385
- }),
386
- ])
387
- : renderSegments(reconciled.mainSegments, renderOptions));
388
-
389
- // Final abort check before committing - another navigation may have started
428
+ abortPromise,
429
+ ]);
430
+ } finally {
431
+ if (onAbort) signal.removeEventListener("abort", onAbort);
432
+ }
433
+ } else {
434
+ newTree = await renderSegments(reconciled.mainSegments, renderOptions);
435
+ }
436
+
390
437
  if (signal?.aborted) {
391
438
  debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
392
439
  return;
393
440
  }
394
441
 
395
- // Check if this is an intercept response (any slot is active)
396
442
  const isInterceptResponse = hasActiveInterceptSlots(
397
443
  payload.metadata?.slots,
398
444
  );
399
445
 
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
446
  const effectiveInterceptSource =
405
447
  interceptSourceUrl || segmentState.currentUrl;
406
448
  if (mode.type !== "action" && mode.type !== "stale-revalidation") {
@@ -411,8 +453,7 @@ export function createPartialUpdater(
411
453
  }
412
454
  }
413
455
 
414
- // Commit navigation - transaction handles all store mutations atomically
415
- const allSegmentIds = reconciled.segments.map((s) => s.id);
456
+ const allSegmentIds = matchedIds;
416
457
  const serverLocationState = payload.metadata?.locationState;
417
458
  const overrides: CommitOverrides | undefined = isInterceptResponse
418
459
  ? {
@@ -424,9 +465,12 @@ export function createPartialUpdater(
424
465
  : serverLocationState
425
466
  ? { serverState: serverLocationState }
426
467
  : undefined;
427
- tx.commit(allSegmentIds, reconciled.segments, overrides);
468
+ const { scroll: navScroll } = tx.commit(
469
+ allSegmentIds,
470
+ reconciled.segments,
471
+ overrides,
472
+ );
428
473
 
429
- // For stale revalidation: verify history key hasn't changed before updating UI
430
474
  if (mode.type === "stale-revalidation") {
431
475
  const historyKeyNow = store.getHistoryKey();
432
476
  if (historyKeyNow !== historyKeyAtStart) {
@@ -439,8 +483,8 @@ export function createPartialUpdater(
439
483
 
440
484
  debugLog("[partial-update] updating document");
441
485
 
442
- // Emit update to trigger React render
443
- const hasTransition = reconciled.mainSegments.some((s) => s.transition);
486
+ const hasTransition = shouldStartViewTransition(reconciled.segments);
487
+ const scrollPayload = toScrollPayload(navScroll);
444
488
 
445
489
  if (mode.type === "action" || mode.type === "stale-revalidation") {
446
490
  startTransition(() => {
@@ -450,6 +494,7 @@ export function createPartialUpdater(
450
494
  onUpdate({
451
495
  root: newTree,
452
496
  metadata: payload.metadata!,
497
+ scroll: scrollPayload,
453
498
  });
454
499
  });
455
500
  } else if (hasTransition) {
@@ -460,19 +505,34 @@ export function createPartialUpdater(
460
505
  onUpdate({
461
506
  root: newTree,
462
507
  metadata: payload.metadata!,
508
+ scroll: scrollPayload,
509
+ });
510
+ });
511
+ } else if (fullyPrefetched) {
512
+ // Fully-prefetched navigation: the payload is already fully resolved, so
513
+ // commit inside a transition to hold the current UI across the synchronous
514
+ // use()/Suspense resolution and avoid a 1-frame loading()/fallback flash.
515
+ // No addTransitionType: this is not a view transition, just the React
516
+ // content-hold. Cold and partially-prefetched navs do NOT reach here, so
517
+ // their fallbacks still stream (see fullyPrefetched in navigation-client).
518
+ startTransition(() => {
519
+ onUpdate({
520
+ root: newTree,
521
+ metadata: payload.metadata!,
522
+ scroll: scrollPayload,
463
523
  });
464
524
  });
465
525
  } else {
466
526
  onUpdate({
467
527
  root: newTree,
468
528
  metadata: payload.metadata!,
529
+ scroll: scrollPayload,
469
530
  });
470
531
  }
471
532
 
472
533
  debugLog("[Browser] Navigation complete");
473
534
  return;
474
535
  } else {
475
- // Full update (fallback)
476
536
  console.warn(`[Browser] Full update (fallback)`);
477
537
 
478
538
  const segments = payload.metadata?.segments || [];
@@ -492,18 +552,29 @@ export function createPartialUpdater(
492
552
  }
493
553
 
494
554
  const fullUpdateServerState = payload.metadata?.locationState;
495
- if (fullUpdateServerState) {
496
- tx.commit(segmentIds, segments, { serverState: fullUpdateServerState });
497
- } else {
498
- tx.commit(segmentIds, segments);
499
- }
555
+ const { scroll: fullScroll } = fullUpdateServerState
556
+ ? tx.commit(segmentIds, segments, {
557
+ serverState: fullUpdateServerState,
558
+ })
559
+ : tx.commit(segmentIds, segments);
500
560
 
501
- const fullHasTransition = segments.some(
502
- (s: ResolvedSegment) => s.transition,
503
- );
561
+ const fullHasTransition = shouldStartViewTransition(segments);
562
+ const fullScrollPayload = toScrollPayload(fullScroll);
504
563
 
505
564
  if (mode.type === "stale-revalidation") {
506
565
  await rawStreamComplete;
566
+ // Mirror the partial branch's history-key staleness guard (above): the
567
+ // await above is a real async suspension, so the user may have navigated
568
+ // away while this background revalidation was draining. Dropping a late
569
+ // full-update here prevents it from clobbering the freshly committed UI
570
+ // of the page the user moved to.
571
+ const historyKeyNow = store.getHistoryKey();
572
+ if (historyKeyNow !== historyKeyAtStart) {
573
+ debugLog(
574
+ `[Browser] Stale revalidation (full update): history key changed (${historyKeyAtStart} -> ${historyKeyNow}), skipping UI update`,
575
+ );
576
+ return;
577
+ }
507
578
  startTransition(() => {
508
579
  if (fullHasTransition && addTransitionType) {
509
580
  addTransitionType("action");
@@ -511,16 +582,18 @@ export function createPartialUpdater(
511
582
  onUpdate({
512
583
  root: newTree,
513
584
  metadata: payload.metadata!,
585
+ scroll: fullScrollPayload,
514
586
  });
515
587
  });
516
588
  } else if (mode.type === "action") {
517
- startTransition(async () => {
589
+ startTransition(() => {
518
590
  if (fullHasTransition && addTransitionType) {
519
591
  addTransitionType("action");
520
592
  }
521
593
  onUpdate({
522
594
  root: newTree,
523
595
  metadata: payload.metadata!,
596
+ scroll: fullScrollPayload,
524
597
  });
525
598
  });
526
599
  } else if (fullHasTransition) {
@@ -531,12 +604,14 @@ export function createPartialUpdater(
531
604
  onUpdate({
532
605
  root: newTree,
533
606
  metadata: payload.metadata!,
607
+ scroll: fullScrollPayload,
534
608
  });
535
609
  });
536
610
  } else {
537
611
  onUpdate({
538
612
  root: newTree,
539
613
  metadata: payload.metadata!,
614
+ scroll: fullScrollPayload,
540
615
  });
541
616
  }
542
617