@rangojs/router 0.0.0-experimental.a769fbe7 → 0.0.0-experimental.ac99d918

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 (374) hide show
  1. package/README.md +211 -43
  2. package/dist/bin/rango.js +279 -102
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +3313 -1160
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +62 -11
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +63 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +222 -30
  12. package/skills/caching/SKILL.md +263 -8
  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 +250 -30
  18. package/skills/host-router/SKILL.md +124 -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 +223 -9
  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 +77 -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 +66 -5
  35. package/skills/router-setup/SKILL.md +38 -3
  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 +319 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/vercel/SKILL.md +107 -0
  57. package/skills/view-transitions/SKILL.md +294 -0
  58. package/src/__augment-tests__/augment.ts +81 -0
  59. package/src/__augment-tests__/augmented.check.ts +116 -0
  60. package/src/__internal.ts +1 -66
  61. package/src/browser/action-coordinator.ts +53 -36
  62. package/src/browser/action-fence.ts +47 -0
  63. package/src/browser/app-shell.ts +39 -0
  64. package/src/browser/app-version.ts +14 -0
  65. package/src/browser/cookie-name.ts +140 -0
  66. package/src/browser/event-controller.ts +81 -147
  67. package/src/browser/history-state.ts +21 -0
  68. package/src/browser/index.ts +3 -3
  69. package/src/browser/invalidate-client-cache.ts +52 -0
  70. package/src/browser/navigation-bridge.ts +66 -14
  71. package/src/browser/navigation-client.ts +172 -109
  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 +9 -59
  75. package/src/browser/partial-update.ts +79 -93
  76. package/src/browser/prefetch/cache.ts +180 -62
  77. package/src/browser/prefetch/fetch.ts +252 -39
  78. package/src/browser/prefetch/queue.ts +42 -8
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +72 -10
  81. package/src/browser/react/NavigationProvider.tsx +83 -31
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/context.ts +7 -2
  84. package/src/browser/react/filter-segment-order.ts +49 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +166 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +23 -69
  90. package/src/browser/react/use-link-status.ts +0 -4
  91. package/src/browser/react/use-navigation.ts +22 -5
  92. package/src/browser/react/use-params.ts +20 -10
  93. package/src/browser/react/use-reverse.ts +106 -0
  94. package/src/browser/react/use-router.ts +46 -11
  95. package/src/browser/react/use-search-params.ts +0 -5
  96. package/src/browser/react/use-segments.ts +11 -21
  97. package/src/browser/response-adapter.ts +52 -1
  98. package/src/browser/rsc-router.tsx +111 -24
  99. package/src/browser/scroll-restoration.ts +29 -19
  100. package/src/browser/segment-reconciler.ts +36 -14
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +176 -62
  103. package/src/browser/types.ts +60 -11
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +65 -40
  107. package/src/build/generate-route-types.ts +6 -0
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +137 -32
  111. package/src/build/route-types/codegen.ts +4 -4
  112. package/src/build/route-types/include-resolution.ts +9 -2
  113. package/src/build/route-types/param-extraction.ts +6 -3
  114. package/src/build/route-types/per-module-writer.ts +7 -4
  115. package/src/build/route-types/router-processing.ts +333 -94
  116. package/src/build/route-types/scan-filter.ts +9 -2
  117. package/src/build/route-types/source-scan.ts +118 -0
  118. package/src/build/runtime-discovery.ts +9 -20
  119. package/src/cache/cache-error.ts +104 -0
  120. package/src/cache/cache-policy.ts +68 -28
  121. package/src/cache/cache-runtime.ts +134 -32
  122. package/src/cache/cache-scope.ts +100 -74
  123. package/src/cache/cache-tag.ts +98 -0
  124. package/src/cache/cf/cf-cache-store.ts +2256 -241
  125. package/src/cache/cf/index.ts +6 -16
  126. package/src/cache/document-cache.ts +61 -20
  127. package/src/cache/handle-snapshot.ts +63 -0
  128. package/src/cache/index.ts +22 -20
  129. package/src/cache/memory-segment-store.ts +136 -37
  130. package/src/cache/profile-registry.ts +6 -30
  131. package/src/cache/read-through-swr.ts +41 -11
  132. package/src/cache/segment-codec.ts +0 -16
  133. package/src/cache/tag-invalidation.ts +230 -0
  134. package/src/cache/types.ts +33 -100
  135. package/src/cache/vercel/index.ts +11 -0
  136. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  137. package/src/client.rsc.tsx +6 -21
  138. package/src/client.tsx +108 -290
  139. package/src/component-utils.ts +19 -0
  140. package/src/context-var.ts +17 -5
  141. package/src/decode-loader-results.ts +36 -0
  142. package/src/defer.ts +196 -0
  143. package/src/deps/browser.ts +0 -1
  144. package/src/deps/ssr.ts +0 -1
  145. package/src/errors.ts +30 -4
  146. package/src/handle.ts +70 -22
  147. package/src/handles/MetaTags.tsx +0 -14
  148. package/src/handles/breadcrumbs.ts +16 -5
  149. package/src/handles/meta.ts +0 -39
  150. package/src/host/cookie-handler.ts +0 -36
  151. package/src/host/errors.ts +0 -24
  152. package/src/host/index.ts +8 -2
  153. package/src/host/pattern-matcher.ts +7 -50
  154. package/src/host/router.ts +107 -99
  155. package/src/host/testing.ts +40 -27
  156. package/src/host/types.ts +37 -4
  157. package/src/host/utils.ts +1 -1
  158. package/src/href-client.ts +137 -22
  159. package/src/index.rsc.ts +69 -10
  160. package/src/index.ts +112 -14
  161. package/src/internal-debug.ts +2 -4
  162. package/src/loader-store.ts +500 -0
  163. package/src/loader.rsc.ts +20 -13
  164. package/src/loader.ts +12 -11
  165. package/src/missing-id-error.ts +68 -0
  166. package/src/network-error-thrower.tsx +1 -6
  167. package/src/outlet-context.ts +1 -1
  168. package/src/outlet-provider.tsx +1 -5
  169. package/src/prerender/param-hash.ts +10 -11
  170. package/src/prerender/store.ts +37 -41
  171. package/src/prerender.ts +198 -82
  172. package/src/redirect-origin.ts +100 -0
  173. package/src/response-utils.ts +37 -0
  174. package/src/reverse.ts +65 -15
  175. package/src/root-error-boundary.tsx +1 -19
  176. package/src/route-content-wrapper.tsx +7 -72
  177. package/src/route-definition/dsl-helpers.ts +413 -275
  178. package/src/route-definition/helper-factories.ts +29 -139
  179. package/src/route-definition/helpers-types.ts +107 -32
  180. package/src/route-definition/index.ts +3 -0
  181. package/src/route-definition/redirect.ts +50 -8
  182. package/src/route-definition/resolve-handler-use.ts +161 -0
  183. package/src/route-definition/use-item-types.ts +32 -0
  184. package/src/route-map-builder.ts +0 -16
  185. package/src/route-types.ts +37 -41
  186. package/src/router/basename.ts +14 -0
  187. package/src/router/content-negotiation.ts +108 -9
  188. package/src/router/error-handling.ts +13 -17
  189. package/src/router/find-match.ts +44 -23
  190. package/src/router/handler-context.ts +46 -30
  191. package/src/router/intercept-resolution.ts +23 -23
  192. package/src/router/lazy-includes.ts +15 -52
  193. package/src/router/loader-resolution.ts +207 -30
  194. package/src/router/logging.ts +0 -6
  195. package/src/router/manifest.ts +40 -42
  196. package/src/router/match-api.ts +120 -204
  197. package/src/router/match-context.ts +0 -22
  198. package/src/router/match-handlers.ts +58 -58
  199. package/src/router/match-middleware/background-revalidation.ts +0 -7
  200. package/src/router/match-middleware/cache-lookup.ts +161 -262
  201. package/src/router/match-middleware/cache-store.ts +3 -33
  202. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  203. package/src/router/match-middleware/segment-resolution.ts +45 -14
  204. package/src/router/match-pipelines.ts +1 -42
  205. package/src/router/match-result.ts +87 -39
  206. package/src/router/metrics.ts +0 -34
  207. package/src/router/middleware-types.ts +7 -140
  208. package/src/router/middleware.ts +169 -140
  209. package/src/router/navigation-snapshot.ts +131 -0
  210. package/src/router/params-util.ts +23 -0
  211. package/src/router/pattern-matching.ts +109 -63
  212. package/src/router/prerender-match.ts +190 -54
  213. package/src/router/preview-match.ts +32 -102
  214. package/src/router/request-classification.ts +276 -0
  215. package/src/router/revalidation.ts +63 -55
  216. package/src/router/route-snapshot.ts +244 -0
  217. package/src/router/router-context.ts +0 -27
  218. package/src/router/router-interfaces.ts +100 -35
  219. package/src/router/router-options.ts +91 -11
  220. package/src/router/router-registry.ts +2 -5
  221. package/src/router/segment-resolution/fresh.ts +119 -65
  222. package/src/router/segment-resolution/helpers.ts +34 -0
  223. package/src/router/segment-resolution/loader-cache.ts +40 -37
  224. package/src/router/segment-resolution/revalidation.ts +329 -305
  225. package/src/router/segment-resolution/static-store.ts +19 -5
  226. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  227. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  228. package/src/router/segment-resolution.ts +4 -1
  229. package/src/router/segment-wrappers.ts +0 -3
  230. package/src/router/state-cookie-name.ts +33 -0
  231. package/src/router/substitute-pattern-params.ts +56 -0
  232. package/src/router/telemetry-otel.ts +0 -20
  233. package/src/router/telemetry.ts +96 -19
  234. package/src/router/timeout.ts +0 -20
  235. package/src/router/trie-matching.ts +91 -46
  236. package/src/router/types.ts +9 -63
  237. package/src/router/url-params.ts +44 -0
  238. package/src/router.ts +128 -42
  239. package/src/rsc/handler-context.ts +3 -2
  240. package/src/rsc/handler.ts +492 -409
  241. package/src/rsc/helpers.ts +162 -46
  242. package/src/rsc/index.ts +1 -1
  243. package/src/rsc/json-route-result.ts +38 -0
  244. package/src/rsc/loader-fetch.ts +18 -3
  245. package/src/rsc/manifest-init.ts +33 -42
  246. package/src/rsc/origin-guard.ts +39 -25
  247. package/src/rsc/progressive-enhancement.ts +28 -4
  248. package/src/rsc/redirect-guard.ts +99 -0
  249. package/src/rsc/response-error.ts +79 -12
  250. package/src/rsc/response-route-handler.ts +90 -63
  251. package/src/rsc/rsc-rendering.ts +53 -56
  252. package/src/rsc/runtime-warnings.ts +23 -10
  253. package/src/rsc/server-action.ts +74 -69
  254. package/src/rsc/ssr-setup.ts +18 -2
  255. package/src/rsc/types.ts +22 -9
  256. package/src/runtime-env.ts +18 -0
  257. package/src/search-params.ts +4 -20
  258. package/src/segment-content-promise.ts +67 -0
  259. package/src/segment-loader-promise.ts +134 -0
  260. package/src/segment-system.tsx +208 -201
  261. package/src/serialize.ts +243 -0
  262. package/src/server/context.ts +211 -52
  263. package/src/server/cookie-store.ts +80 -5
  264. package/src/server/handle-store.ts +26 -24
  265. package/src/server/loader-registry.ts +10 -28
  266. package/src/server/request-context.ts +289 -124
  267. package/src/ssr/index.tsx +22 -15
  268. package/src/static-handler.ts +27 -18
  269. package/src/testing/cache-status.ts +162 -0
  270. package/src/testing/collect-handle.ts +40 -0
  271. package/src/testing/dispatch.ts +618 -0
  272. package/src/testing/dom.entry.ts +22 -0
  273. package/src/testing/e2e/fixture.ts +188 -0
  274. package/src/testing/e2e/index.ts +128 -0
  275. package/src/testing/e2e/matchers.ts +35 -0
  276. package/src/testing/e2e/page-helpers.ts +272 -0
  277. package/src/testing/e2e/parity.ts +387 -0
  278. package/src/testing/e2e/server.ts +195 -0
  279. package/src/testing/flight-matchers.ts +97 -0
  280. package/src/testing/flight-normalize.ts +11 -0
  281. package/src/testing/flight-runtime.d.ts +57 -0
  282. package/src/testing/flight-tree.ts +682 -0
  283. package/src/testing/flight.entry.ts +52 -0
  284. package/src/testing/flight.ts +232 -0
  285. package/src/testing/generated-routes.ts +183 -0
  286. package/src/testing/index.ts +99 -0
  287. package/src/testing/internal/context.ts +348 -0
  288. package/src/testing/internal/flight-client-globals.ts +30 -0
  289. package/src/testing/internal/seed-vars.ts +54 -0
  290. package/src/testing/render-handler.ts +330 -0
  291. package/src/testing/render-route.tsx +566 -0
  292. package/src/testing/run-loader.ts +378 -0
  293. package/src/testing/run-middleware.ts +205 -0
  294. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  295. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  297. package/src/testing/vitest-stubs/version.ts +5 -0
  298. package/src/testing/vitest.ts +305 -0
  299. package/src/theme/ThemeProvider.tsx +0 -52
  300. package/src/theme/ThemeScript.tsx +0 -6
  301. package/src/theme/constants.ts +0 -12
  302. package/src/theme/index.ts +0 -7
  303. package/src/theme/theme-context.ts +1 -5
  304. package/src/theme/theme-script.ts +0 -14
  305. package/src/theme/use-theme.ts +0 -3
  306. package/src/types/boundaries.ts +0 -35
  307. package/src/types/cache-types.ts +17 -8
  308. package/src/types/error-types.ts +30 -90
  309. package/src/types/global-namespace.ts +54 -41
  310. package/src/types/handler-context.ts +124 -70
  311. package/src/types/index.ts +1 -10
  312. package/src/types/loader-types.ts +40 -11
  313. package/src/types/request-scope.ts +107 -0
  314. package/src/types/route-config.ts +6 -50
  315. package/src/types/route-entry.ts +12 -7
  316. package/src/types/segments.ts +36 -15
  317. package/src/urls/include-helper.ts +33 -70
  318. package/src/urls/index.ts +1 -11
  319. package/src/urls/path-helper-types.ts +58 -11
  320. package/src/urls/path-helper.ts +57 -111
  321. package/src/urls/pattern-types.ts +48 -19
  322. package/src/urls/response-types.ts +25 -22
  323. package/src/urls/type-extraction.ts +58 -139
  324. package/src/urls/urls-function.ts +1 -18
  325. package/src/use-loader.tsx +346 -89
  326. package/src/vite/debug.ts +185 -0
  327. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  328. package/src/vite/discovery/discover-routers.ts +130 -85
  329. package/src/vite/discovery/discovery-errors.ts +194 -0
  330. package/src/vite/discovery/gate-state.ts +171 -0
  331. package/src/vite/discovery/prerender-collection.ts +192 -99
  332. package/src/vite/discovery/route-types-writer.ts +40 -84
  333. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  334. package/src/vite/discovery/state.ts +51 -4
  335. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  336. package/src/vite/index.ts +8 -0
  337. package/src/vite/plugin-types.ts +236 -6
  338. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  339. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  340. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  341. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  342. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  343. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  344. package/src/vite/plugins/expose-action-id.ts +48 -95
  345. package/src/vite/plugins/expose-id-utils.ts +11 -50
  346. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  347. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  348. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  349. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  350. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  351. package/src/vite/plugins/performance-tracks.ts +65 -207
  352. package/src/vite/plugins/refresh-cmd.ts +89 -27
  353. package/src/vite/plugins/use-cache-transform.ts +73 -83
  354. package/src/vite/plugins/vercel-output.ts +258 -0
  355. package/src/vite/plugins/version-injector.ts +21 -25
  356. package/src/vite/plugins/version-plugin.ts +32 -23
  357. package/src/vite/plugins/virtual-entries.ts +46 -17
  358. package/src/vite/rango.ts +207 -125
  359. package/src/vite/router-discovery.ts +931 -133
  360. package/src/vite/utils/ast-handler-extract.ts +15 -31
  361. package/src/vite/utils/banner.ts +1 -1
  362. package/src/vite/utils/bundle-analysis.ts +10 -15
  363. package/src/vite/utils/client-chunks.ts +184 -0
  364. package/src/vite/utils/forward-user-plugins.ts +171 -0
  365. package/src/vite/utils/manifest-utils.ts +4 -59
  366. package/src/vite/utils/package-resolution.ts +20 -52
  367. package/src/vite/utils/prerender-utils.ts +27 -29
  368. package/src/vite/utils/shared-utils.ts +97 -44
  369. package/src/browser/action-response-classifier.ts +0 -99
  370. package/src/browser/debug-channel.ts +0 -93
  371. package/src/browser/react/use-client-cache.ts +0 -58
  372. package/src/browser/shallow.ts +0 -40
  373. package/src/handles/index.ts +0 -7
  374. package/src/router/middleware-cookies.ts +0 -55
@@ -11,9 +11,8 @@ import {
11
11
  } from "./scroll-restoration.js";
12
12
  import type { EventController, NavigationHandle } from "./event-controller.js";
13
13
  import { debugLog } from "./logging.js";
14
- import { buildHistoryState } from "./history-state.js";
14
+ import { buildHistoryState, pushHistoryWithIdx } from "./history-state.js";
15
15
 
16
- // Re-export for consumers that import from navigation-transaction
17
16
  export { resolveNavigationState } from "./history-state.js";
18
17
 
19
18
  /** Check if a history state object contains location state keys. */
@@ -25,7 +24,6 @@ function hasLocationState(state: unknown): boolean {
25
24
  );
26
25
  }
27
26
 
28
- // Polyfill Symbol.dispose for Safari and older browsers
29
27
  if (typeof Symbol.dispose === "undefined") {
30
28
  (Symbol as any).dispose = Symbol("Symbol.dispose");
31
29
  }
@@ -114,7 +112,6 @@ export function createNavigationTransaction(
114
112
  let committed = false;
115
113
  const currentUrl = window.location.href;
116
114
 
117
- // Start navigation in event controller (this sets loading state)
118
115
  const handle = eventController.startNavigation(url, options);
119
116
 
120
117
  /**
@@ -138,76 +135,50 @@ export function createNavigationTransaction(
138
135
 
139
136
  const parsedUrl = new URL(url, window.location.origin);
140
137
 
141
- // Generate history key from URL (with intercept suffix for separate caching)
142
138
  const historyKey = generateHistoryKey(url, { intercept });
143
139
 
144
- // For cache-only commits (stale revalidation), only update cache and return
145
- // Don't touch store state or history - user may have navigated elsewhere
146
140
  if (cacheOnly) {
147
141
  const currentHandleData = eventController.getHandleState().data;
148
142
  store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
149
- // Complete the navigation handle so currentNavigation is cleared.
150
- // Without this, the entry lingers and weakens state-machine invariants.
151
143
  handle.complete(parsedUrl);
152
144
  debugLog("[Browser] Cache-only commit, historyKey:", historyKey);
153
145
  return { scroll: false };
154
146
  }
155
147
 
156
- // Save current scroll position before navigating
157
148
  handleNavigationStart();
158
149
 
159
- // Update segment state atomically
160
150
  store.setSegmentIds(segmentIds);
161
151
  store.setCurrentUrl(url);
162
152
  store.setPath(parsedUrl.pathname);
163
153
 
164
154
  store.setHistoryKey(historyKey);
165
155
 
166
- // Cache segments with current handleData for this history entry
167
156
  const currentHandleData = eventController.getHandleState().data;
168
157
  store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
169
158
 
170
- // For server actions, skip URL/history updates but still complete navigation
171
159
  if (storeOnly) {
172
160
  debugLog("[Browser] Store updated (action)");
173
- // Complete navigation to clear loading state
174
161
  handle.complete(parsedUrl);
175
162
  return { scroll: false };
176
163
  }
177
164
 
178
- // Build history state - include user state, intercept info, and server-set state
179
165
  const historyState = buildHistoryState(
180
166
  opts.state,
181
167
  { intercept, sourceUrl: interceptSourceUrl },
182
168
  serverState,
183
169
  );
184
170
 
185
- // Snapshot old state before pushState/replaceState overwrites it.
186
- // Used to detect when location state is being cleared.
187
171
  const oldState = window.history.state;
188
172
 
189
- // Update browser URL
190
- if (replace) {
191
- window.history.replaceState(historyState, "", url);
192
- } else {
193
- window.history.pushState(historyState, "", url);
194
- }
195
- // Ensure new history entry has a scroll restoration key
173
+ pushHistoryWithIdx(historyState, url, replace ?? false);
196
174
  ensureHistoryKey();
197
175
 
198
- // Notify location state hooks when either old or new state carries
199
- // location state. This covers both "set new state" and "clear old state"
200
- // for same-page navigations where components don't remount.
201
176
  if (hasLocationState(oldState) || hasLocationState(historyState)) {
202
177
  window.dispatchEvent(new Event("__rsc_locationstate"));
203
178
  }
204
179
 
205
- // Complete the navigation in event controller (sets idle state, updates location)
206
180
  handle.complete(parsedUrl);
207
181
 
208
- // NOTE: Scroll is NOT handled here. The caller (partial-update.ts) handles
209
- // scroll AFTER onUpdate() so React has the new content before we scroll.
210
-
211
182
  debugLog(
212
183
  "[Browser] Navigation committed, historyKey:",
213
184
  historyKey,
@@ -221,10 +192,6 @@ export function createNavigationTransaction(
221
192
  handle,
222
193
  commit,
223
194
 
224
- /**
225
- * Create a bound transaction with pre-configured URL options
226
- * segmentIds and segments provided at commit time (after they're resolved)
227
- */
228
195
  with(
229
196
  opts: Omit<CommitOptions, "segmentIds" | "segments">,
230
197
  ): BoundTransaction {
@@ -240,30 +207,16 @@ export function createNavigationTransaction(
240
207
  segments: ResolvedSegment[],
241
208
  overrides?: BoundCommitOverrides,
242
209
  ) => {
243
- // Allow overrides to disable scroll (e.g., for intercepts)
244
- const finalScroll =
245
- overrides?.scroll !== undefined ? overrides.scroll : opts.scroll;
246
- // Allow overrides to force replace (e.g., for intercepts)
247
- const finalReplace =
248
- overrides?.replace !== undefined ? overrides.replace : opts.replace;
249
- // Intercept info: overrides take precedence, fallback to opts
250
- const intercept =
251
- overrides?.intercept !== undefined
252
- ? overrides.intercept
253
- : opts.intercept;
210
+ const finalScroll = overrides?.scroll ?? opts.scroll;
211
+ const finalReplace = overrides?.replace ?? opts.replace;
212
+ const intercept = overrides?.intercept ?? opts.intercept;
254
213
  const interceptSourceUrl =
255
- overrides?.interceptSourceUrl !== undefined
256
- ? overrides.interceptSourceUrl
257
- : opts.interceptSourceUrl;
258
- // Cache-only mode: overrides take precedence, fallback to opts
259
- const cacheOnly =
260
- overrides?.cacheOnly !== undefined
261
- ? overrides.cacheOnly
262
- : opts.cacheOnly;
263
- // User state: overrides take precedence, fallback to opts
214
+ overrides?.interceptSourceUrl ?? opts.interceptSourceUrl;
215
+ const cacheOnly = overrides?.cacheOnly ?? opts.cacheOnly;
216
+ // state is `unknown` (null is meaningful) so `??` would wrongly drop a
217
+ // null override; serverState always comes from overrides, never opts.
264
218
  const state =
265
219
  overrides?.state !== undefined ? overrides.state : opts.state;
266
- // Server-set location state: only from overrides (set by partial-update)
267
220
  const serverState = overrides?.serverState;
268
221
  return commit({
269
222
  ...opts,
@@ -282,13 +235,10 @@ export function createNavigationTransaction(
282
235
  },
283
236
 
284
237
  [Symbol.dispose]() {
285
- // Superseded: another navigation took over.
286
238
  if (handle.signal.aborted) {
287
239
  return;
288
240
  }
289
241
 
290
- // Failed (not committed): keep the target URL -- the error UI owns it.
291
- // Just reset the event controller to idle.
292
242
  if (!committed) {
293
243
  handle[Symbol.dispose]();
294
244
  }
@@ -14,20 +14,35 @@ 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";
22
28
  import type { NavigationUpdate } from "./types.js";
23
29
 
24
- /** Build a scroll payload from the commit's scroll option */
25
30
  function toScrollPayload(
26
31
  scroll: boolean | undefined,
27
32
  ): NonNullable<NavigationUpdate["scroll"]> {
28
33
  return { enabled: scroll !== false ? scroll : false };
29
34
  }
30
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
+ }
45
+
31
46
  /**
32
47
  * Configuration for creating a partial updater
33
48
  */
@@ -39,8 +54,8 @@ export interface PartialUpdateConfig {
39
54
  segments: ResolvedSegment[],
40
55
  options?: RenderSegmentsOptions,
41
56
  ) => Promise<ReactNode> | ReactNode;
42
- /** RSC version received from server (from initial payload metadata) */
43
- version?: string;
57
+ /** RSC version getter returns the current version (may change after HMR) */
58
+ getVersion?: () => string | undefined;
44
59
  }
45
60
 
46
61
  /**
@@ -76,7 +91,7 @@ export type UpdateMode =
76
91
  /** Source URL for intercept restore (popstate cache miss) */
77
92
  interceptSourceUrl?: string;
78
93
  }
79
- | { type: "leave-intercept" }
94
+ | { type: "leave-intercept"; interceptSourceUrl?: string }
80
95
  | { type: "stale-revalidation"; interceptSourceUrl?: string }
81
96
  | { type: "action"; interceptSourceUrl?: string };
82
97
 
@@ -92,35 +107,23 @@ export type PartialUpdater = (
92
107
  mode?: UpdateMode,
93
108
  ) => Promise<void>;
94
109
 
95
- /**
96
- * Create a partial updater for fetching and applying RSC partial updates
97
- *
98
- * This function is shared between navigation-bridge and server-action-bridge
99
- * to handle partial RSC updates with HMR resilience.
100
- *
101
- * @param config - Partial update configuration
102
- * @returns fetchPartialUpdate function
103
- */
104
110
  export function createPartialUpdater(
105
111
  config: PartialUpdateConfig,
106
112
  ): PartialUpdater {
107
- const { store, client, onUpdate, renderSegments, version } = config;
113
+ const {
114
+ store,
115
+ client,
116
+ onUpdate,
117
+ renderSegments,
118
+ getVersion = () => undefined,
119
+ } = config;
108
120
 
109
- /**
110
- * Get current page's cached segments as an array
111
- */
112
121
  function getCurrentCachedSegments(): ResolvedSegment[] {
113
122
  const currentKey = store.getHistoryKey();
114
123
  const cached = store.getCachedSegments(currentKey);
115
124
  return cached?.segments || [];
116
125
  }
117
126
 
118
- /**
119
- * Fetch partial update and trigger UI update
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,
@@ -132,26 +135,16 @@ export function createPartialUpdater(
132
135
  const segmentState = store.getSegmentState();
133
136
  const url = targetUrl || window.location.href;
134
137
 
135
- // Capture history key at start for stale revalidation consistency check
136
138
  const historyKeyAtStart = store.getHistoryKey();
137
139
 
138
- // Derive interceptSourceUrl from modes that carry it
139
- const interceptSourceUrl =
140
- mode.type === "stale-revalidation" ||
141
- mode.type === "action" ||
142
- mode.type === "navigate"
143
- ? mode.interceptSourceUrl
144
- : undefined;
140
+ const interceptSourceUrl = mode.interceptSourceUrl;
145
141
 
146
- // When leaving intercept, filter out intercept-specific segments
147
142
  let segments: string[];
148
143
  if (mode.type === "leave-intercept") {
149
144
  const currentSegments = segmentIds ?? segmentState.currentSegmentIds;
150
145
  const currentCached = getCurrentCachedSegments();
151
146
  const interceptIds = new Set(
152
- currentCached
153
- .filter((s) => s.namespace?.startsWith("intercept:"))
154
- .map((s) => s.id),
147
+ currentCached.filter(isInterceptSegment).map((s) => s.id),
155
148
  );
156
149
  segments = currentSegments.filter((id) => !interceptIds.has(id));
157
150
  debugLog(
@@ -161,9 +154,10 @@ export function createPartialUpdater(
161
154
  segments = segmentIds ?? segmentState.currentSegmentIds;
162
155
  }
163
156
 
164
- // For intercept revalidation, use the intercept source URL as previousUrl
165
157
  const previousUrl =
166
- interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
158
+ mode.type === "leave-intercept"
159
+ ? segmentState.currentUrl || tx.currentUrl
160
+ : interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
167
161
 
168
162
  debugLog(`\n[Browser] >>> NAVIGATION`);
169
163
  debugLog(`[Browser] From: ${previousUrl}`);
@@ -173,31 +167,26 @@ export function createPartialUpdater(
173
167
  debugLog(`[Browser] Intercept context from: ${interceptSourceUrl}`);
174
168
  }
175
169
 
176
- // Get cached segments for merging with server diff.
177
- // When navigating with targetCacheSegments, use those for consistency.
178
- // Otherwise fall back to current page's segments (for same-route revalidation).
179
170
  const targetCache =
180
- mode.type === "navigate" ? mode.targetCacheSegments : undefined;
181
- const cachedSegs =
182
- targetCache && targetCache.length > 0
183
- ? targetCache
184
- : 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
+ );
185
179
 
186
- // Fetch partial payload (no abort signal - RSC doesn't support it well)
187
180
  let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
188
181
  fetchResult = await client.fetchPartial({
189
182
  targetUrl: url,
190
183
  segmentIds: segments,
191
184
  previousUrl,
192
- // Mark stale when explicitly requested OR when no segments are sent
193
- // (action redirect sends empty segments for a fresh render).
194
185
  staleRevalidation:
195
186
  mode.type === "stale-revalidation" || segments.length === 0,
196
- version,
187
+ version: getVersion(),
188
+ routerId: store.getRouterId?.(),
197
189
  });
198
- // Mark navigation as streaming (response received, now parsing RSC).
199
- // Called after fetchPartial so pendingUrl stays set during the network wait,
200
- // allowing useLinkStatus to show per-link pending indicators.
201
190
  const streamingToken = tx.startStreaming();
202
191
  const { payload, streamComplete: rawStreamComplete } = fetchResult;
203
192
  debugLog("payload.metadata", payload.metadata);
@@ -206,12 +195,43 @@ export function createPartialUpdater(
206
195
  streamingToken.end();
207
196
  });
208
197
 
209
- // 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
+
210
212
  if (payload.metadata?.redirect) {
211
213
  if (signal?.aborted) {
212
214
  debugLog("[Browser] Ignoring stale redirect (aborted)");
213
215
  return;
214
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
+ }
215
235
  const redirectUrl = validateRedirectOrigin(
216
236
  payload.metadata.redirect.url,
217
237
  window.location.origin,
@@ -236,7 +256,6 @@ export function createPartialUpdater(
236
256
  debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
237
257
  debugLog(`[Browser] Diff: ${diff?.join(", ")}`);
238
258
 
239
- // If diff is empty, nothing changed on server side.
240
259
  if (!diff || diff.length === 0) {
241
260
  const matchedIds = matched || [];
242
261
  const cacheMap = new Map(cachedSegs.map((s) => [s.id, s]));
@@ -244,8 +263,7 @@ export function createPartialUpdater(
244
263
  .map((id: string) => cacheMap.get(id))
245
264
  .filter(Boolean) as ResolvedSegment[];
246
265
 
247
- // When navigating with cached segments to a different route, render them.
248
- if (mode.type === "navigate" && targetCache && targetCache.length > 0) {
266
+ if (mode.type === "navigate" && targetCache) {
249
267
  debugLog(
250
268
  "[Browser] No diff but navigating with cached segments - rendering target route",
251
269
  );
@@ -259,10 +277,6 @@ export function createPartialUpdater(
259
277
  existingSegments,
260
278
  );
261
279
 
262
- // Fix: tx.commit() cached the source page's handleData because
263
- // eventController hasn't been updated yet. Overwrite with the
264
- // correct cached handleData to prevent cache corruption on
265
- // subsequent navigations to this same URL.
266
280
  if (mode.targetCacheHandleData) {
267
281
  store.updateCacheHandleData(
268
282
  store.getHistoryKey(),
@@ -270,10 +284,6 @@ export function createPartialUpdater(
270
284
  );
271
285
  }
272
286
 
273
- // Include cachedHandleData in metadata so NavigationProvider can restore
274
- // breadcrumbs and other handle data from cache.
275
- // Remove `handles` from metadata to prevent NavigationProvider from
276
- // processing an empty handles stream, which would clear the cached breadcrumbs.
277
287
  const { handles: _unusedHandles, ...metadataWithoutHandles } =
278
288
  payload.metadata!;
279
289
  const cachedUpdate = {
@@ -285,10 +295,7 @@ export function createPartialUpdater(
285
295
  scroll: toScrollPayload(commitScroll),
286
296
  };
287
297
 
288
- const cachedHasTransition = existingSegments.some(
289
- (s) => s.transition,
290
- );
291
- if (cachedHasTransition) {
298
+ if (shouldStartViewTransition(existingSegments)) {
292
299
  startTransition(() => {
293
300
  if (addTransitionType) {
294
301
  addTransitionType("navigation");
@@ -303,7 +310,6 @@ export function createPartialUpdater(
303
310
  return;
304
311
  }
305
312
 
306
- // When leaving intercept, force re-render even with empty diff
307
313
  if (mode.type === "leave-intercept") {
308
314
  debugLog(
309
315
  "[Browser] Leaving intercept - forcing re-render to remove modal",
@@ -328,7 +334,6 @@ export function createPartialUpdater(
328
334
  return;
329
335
  }
330
336
 
331
- // Same route revalidation with no changes - skip UI update
332
337
  debugLog(
333
338
  "[Browser] No changes - all revalidations returned false, keeping existing UI",
334
339
  );
@@ -337,7 +342,6 @@ export function createPartialUpdater(
337
342
  return;
338
343
  }
339
344
 
340
- // Reconcile server segments with cached segments (single source of truth)
341
345
  const matchedIds = matched || [];
342
346
  const actor: ReconcileActor =
343
347
  mode.type === "stale-revalidation" || mode.type === "action"
@@ -353,7 +357,6 @@ export function createPartialUpdater(
353
357
  insertMissingDiff: true,
354
358
  });
355
359
 
356
- // HMR RESILIENCE: Check if we're missing any matched segments
357
360
  const reconciledIdSet = new Set(reconciled.segments.map((s) => s.id));
358
361
  const missingIds = matchedIds.filter(
359
362
  (id: string) => !reconciledIdSet.has(id),
@@ -381,7 +384,6 @@ export function createPartialUpdater(
381
384
  `[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
382
385
  );
383
386
 
384
- // Refetch with empty segments = server sends everything
385
387
  return fetchPartialUpdate(url, [], true, signal, tx, mode);
386
388
  }
387
389
 
@@ -390,7 +392,6 @@ export function createPartialUpdater(
390
392
  return;
391
393
  }
392
394
 
393
- // Rebuild tree on client (await for loader data resolution)
394
395
  const renderOptions = {
395
396
  isAction: mode.type === "action",
396
397
  forceAwait: mode.type === "stale-revalidation",
@@ -413,21 +414,15 @@ export function createPartialUpdater(
413
414
  ])
414
415
  : renderSegments(reconciled.mainSegments, renderOptions));
415
416
 
416
- // Final abort check before committing - another navigation may have started
417
417
  if (signal?.aborted) {
418
418
  debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
419
419
  return;
420
420
  }
421
421
 
422
- // Check if this is an intercept response (any slot is active)
423
422
  const isInterceptResponse = hasActiveInterceptSlots(
424
423
  payload.metadata?.slots,
425
424
  );
426
425
 
427
- // Track intercept context (only on navigation, not actions or stale revalidation)
428
- // Use the authoritative source from mode/history state when restoring an
429
- // intercept via popstate cache miss; fall back to the current URL for fresh
430
- // intercept navigations.
431
426
  const effectiveInterceptSource =
432
427
  interceptSourceUrl || segmentState.currentUrl;
433
428
  if (mode.type !== "action" && mode.type !== "stale-revalidation") {
@@ -438,9 +433,6 @@ export function createPartialUpdater(
438
433
  }
439
434
  }
440
435
 
441
- // Commit navigation - use server's matched as the authoritative segment ID list.
442
- // reconciled.segments may be missing IDs (e.g., loader segments not in diff or cache)
443
- // but the server's matched always includes all expected segment IDs.
444
436
  const allSegmentIds = matchedIds;
445
437
  const serverLocationState = payload.metadata?.locationState;
446
438
  const overrides: CommitOverrides | undefined = isInterceptResponse
@@ -459,7 +451,6 @@ export function createPartialUpdater(
459
451
  overrides,
460
452
  );
461
453
 
462
- // For stale revalidation: verify history key hasn't changed before updating UI
463
454
  if (mode.type === "stale-revalidation") {
464
455
  const historyKeyNow = store.getHistoryKey();
465
456
  if (historyKeyNow !== historyKeyAtStart) {
@@ -472,9 +463,7 @@ export function createPartialUpdater(
472
463
 
473
464
  debugLog("[partial-update] updating document");
474
465
 
475
- // Emit update to trigger React render.
476
- // Scroll info is included so NavigationProvider applies it after React commits.
477
- const hasTransition = reconciled.mainSegments.some((s) => s.transition);
466
+ const hasTransition = shouldStartViewTransition(reconciled.segments);
478
467
  const scrollPayload = toScrollPayload(navScroll);
479
468
 
480
469
  if (mode.type === "action" || mode.type === "stale-revalidation") {
@@ -510,7 +499,6 @@ export function createPartialUpdater(
510
499
  debugLog("[Browser] Navigation complete");
511
500
  return;
512
501
  } else {
513
- // Full update (fallback)
514
502
  console.warn(`[Browser] Full update (fallback)`);
515
503
 
516
504
  const segments = payload.metadata?.segments || [];
@@ -536,9 +524,7 @@ export function createPartialUpdater(
536
524
  })
537
525
  : tx.commit(segmentIds, segments);
538
526
 
539
- const fullHasTransition = segments.some(
540
- (s: ResolvedSegment) => s.transition,
541
- );
527
+ const fullHasTransition = shouldStartViewTransition(segments);
542
528
  const fullScrollPayload = toScrollPayload(fullScroll);
543
529
 
544
530
  if (mode.type === "stale-revalidation") {