@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650

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