@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c

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 (392) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +126 -16
  3. package/dist/bin/rango.js +319 -95
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +2724 -1053
  6. package/package.json +68 -14
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +64 -2
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +224 -32
  11. package/skills/caching/SKILL.md +279 -17
  12. package/skills/composability/SKILL.md +27 -3
  13. package/skills/css/SKILL.md +76 -0
  14. package/skills/debug-manifest/SKILL.md +4 -2
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +11 -9
  17. package/skills/hooks/SKILL.md +243 -29
  18. package/skills/host-router/SKILL.md +83 -23
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +68 -19
  21. package/skills/layout/SKILL.md +13 -9
  22. package/skills/links/SKILL.md +190 -23
  23. package/skills/loader/SKILL.md +235 -9
  24. package/skills/middleware/SKILL.md +18 -10
  25. package/skills/migrate-nextjs/SKILL.md +43 -19
  26. package/skills/migrate-react-router/SKILL.md +8 -2
  27. package/skills/mime-routes/SKILL.md +28 -1
  28. package/skills/observability/SKILL.md +172 -0
  29. package/skills/parallel/SKILL.md +18 -7
  30. package/skills/prerender/SKILL.md +65 -60
  31. package/skills/rango/SKILL.md +251 -24
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +115 -48
  34. package/skills/route/SKILL.md +46 -5
  35. package/skills/router-setup/SKILL.md +30 -8
  36. package/skills/scripts/SKILL.md +179 -0
  37. package/skills/server-actions/SKILL.md +775 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +130 -0
  40. package/skills/testing/bindings.md +103 -0
  41. package/skills/testing/cache-prerender.md +127 -0
  42. package/skills/testing/client-components.md +124 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +91 -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 +122 -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 +123 -0
  54. package/skills/typesafety/SKILL.md +322 -29
  55. package/skills/use-cache/SKILL.md +57 -14
  56. package/skills/view-transitions/SKILL.md +337 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +0 -65
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/connection-warmup.ts +134 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +192 -150
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/navigation-bridge.ts +94 -25
  70. package/src/browser/navigation-client.ts +121 -84
  71. package/src/browser/navigation-store-handle.ts +38 -0
  72. package/src/browser/navigation-store.ts +115 -67
  73. package/src/browser/navigation-transaction.ts +9 -59
  74. package/src/browser/network-error-handler.ts +34 -7
  75. package/src/browser/partial-update.ts +147 -128
  76. package/src/browser/prefetch/cache.ts +107 -56
  77. package/src/browser/prefetch/fetch.ts +204 -34
  78. package/src/browser/prefetch/queue.ts +6 -3
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +30 -7
  81. package/src/browser/react/NavigationProvider.tsx +283 -118
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  84. package/src/browser/react/filter-segment-order.ts +66 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +178 -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 +17 -14
  90. package/src/browser/react/use-href.tsx +8 -1
  91. package/src/browser/react/use-link-status.ts +33 -8
  92. package/src/browser/react/use-navigation.ts +10 -5
  93. package/src/browser/react/use-params.ts +11 -11
  94. package/src/browser/react/use-reverse.ts +106 -0
  95. package/src/browser/react/use-router.ts +25 -3
  96. package/src/browser/react/use-search-params.ts +0 -5
  97. package/src/browser/react/use-segments.ts +11 -21
  98. package/src/browser/response-adapter.ts +99 -8
  99. package/src/browser/rsc-router.tsx +91 -24
  100. package/src/browser/scroll-restoration.ts +30 -17
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +214 -55
  103. package/src/browser/types.ts +80 -9
  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 +60 -35
  107. package/src/build/generate-route-types.ts +2 -1
  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 +117 -14
  111. package/src/build/route-types/ast-route-extraction.ts +15 -8
  112. package/src/build/route-types/codegen.ts +16 -5
  113. package/src/build/route-types/include-resolution.ts +117 -23
  114. package/src/build/route-types/param-extraction.ts +6 -3
  115. package/src/build/route-types/per-module-writer.ts +22 -6
  116. package/src/build/route-types/router-processing.ts +55 -28
  117. package/src/build/route-types/scan-filter.ts +1 -1
  118. package/src/build/route-types/source-scan.ts +216 -0
  119. package/src/build/runtime-discovery.ts +9 -20
  120. package/src/cache/cache-error.ts +104 -0
  121. package/src/cache/cache-key-utils.ts +29 -13
  122. package/src/cache/cache-policy.ts +108 -34
  123. package/src/cache/cache-runtime.ts +224 -41
  124. package/src/cache/cache-scope.ts +188 -82
  125. package/src/cache/cache-tag.ts +103 -0
  126. package/src/cache/cf/cf-base64.ts +33 -0
  127. package/src/cache/cf/cf-cache-constants.ts +127 -0
  128. package/src/cache/cf/cf-cache-store.ts +1989 -378
  129. package/src/cache/cf/cf-cache-types.ts +349 -0
  130. package/src/cache/cf/cf-kv-utils.ts +46 -0
  131. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  132. package/src/cache/cf/index.ts +6 -16
  133. package/src/cache/document-cache.ts +89 -21
  134. package/src/cache/handle-snapshot.ts +70 -0
  135. package/src/cache/index.ts +10 -20
  136. package/src/cache/memory-segment-store.ts +136 -37
  137. package/src/cache/profile-registry.ts +46 -31
  138. package/src/cache/read-through-swr.ts +56 -12
  139. package/src/cache/segment-codec.ts +9 -17
  140. package/src/cache/tag-invalidation.ts +230 -0
  141. package/src/cache/types.ts +37 -100
  142. package/src/client.rsc.tsx +44 -21
  143. package/src/client.tsx +36 -61
  144. package/src/cloudflare/index.ts +11 -0
  145. package/src/cloudflare/tracing.ts +109 -0
  146. package/src/component-utils.ts +19 -0
  147. package/src/components/DefaultDocument.tsx +8 -2
  148. package/src/context-var.ts +18 -6
  149. package/src/decode-loader-results.ts +52 -0
  150. package/src/defer.ts +196 -0
  151. package/src/deps/ssr.ts +0 -1
  152. package/src/encode-kv.ts +49 -0
  153. package/src/errors.ts +30 -4
  154. package/src/escape-script.ts +52 -0
  155. package/src/handle.ts +31 -23
  156. package/src/handles/MetaTags.tsx +62 -19
  157. package/src/handles/Scripts.tsx +183 -0
  158. package/src/handles/breadcrumbs.ts +37 -8
  159. package/src/handles/is-thenable.ts +19 -0
  160. package/src/handles/meta.ts +51 -40
  161. package/src/handles/script.ts +244 -0
  162. package/src/host/cookie-handler.ts +9 -60
  163. package/src/host/errors.ts +0 -24
  164. package/src/host/index.ts +8 -2
  165. package/src/host/pattern-matcher.ts +23 -52
  166. package/src/host/router.ts +107 -99
  167. package/src/host/testing.ts +40 -27
  168. package/src/host/types.ts +37 -4
  169. package/src/host/utils.ts +1 -1
  170. package/src/href-client.ts +137 -22
  171. package/src/index.rsc.ts +96 -12
  172. package/src/index.ts +94 -14
  173. package/src/internal-debug.ts +11 -10
  174. package/src/loader-store.ts +500 -0
  175. package/src/loader.rsc.ts +20 -13
  176. package/src/loader.ts +12 -11
  177. package/src/missing-id-error.ts +68 -0
  178. package/src/outlet-context.ts +1 -1
  179. package/src/outlet-provider.tsx +1 -5
  180. package/src/prerender/param-hash.ts +16 -16
  181. package/src/prerender/store.ts +32 -37
  182. package/src/prerender.ts +61 -6
  183. package/src/redirect-origin.ts +100 -0
  184. package/src/regex-escape.ts +8 -0
  185. package/src/render-error-thrower.tsx +20 -0
  186. package/src/response-utils.ts +34 -0
  187. package/src/reverse.ts +65 -40
  188. package/src/root-error-boundary.tsx +1 -19
  189. package/src/route-content-wrapper.tsx +19 -77
  190. package/src/route-definition/dsl-helpers.ts +304 -309
  191. package/src/route-definition/helper-factories.ts +28 -140
  192. package/src/route-definition/helpers-types.ts +82 -55
  193. package/src/route-definition/index.ts +1 -2
  194. package/src/route-definition/redirect.ts +44 -11
  195. package/src/route-definition/resolve-handler-use.ts +12 -1
  196. package/src/route-definition/use-item-types.ts +29 -0
  197. package/src/route-map-builder.ts +0 -16
  198. package/src/route-types.ts +19 -46
  199. package/src/router/basename.ts +14 -0
  200. package/src/router/content-negotiation.ts +73 -25
  201. package/src/router/error-handling.ts +45 -18
  202. package/src/router/find-match.ts +44 -23
  203. package/src/router/handler-context.ts +27 -43
  204. package/src/router/instrument.ts +350 -0
  205. package/src/router/intercept-resolution.ts +39 -20
  206. package/src/router/lazy-includes.ts +10 -47
  207. package/src/router/loader-resolution.ts +155 -72
  208. package/src/router/logging.ts +0 -6
  209. package/src/router/manifest.ts +18 -29
  210. package/src/router/match-api.ts +9 -24
  211. package/src/router/match-context.ts +0 -22
  212. package/src/router/match-handlers.ts +58 -58
  213. package/src/router/match-middleware/background-revalidation.ts +40 -24
  214. package/src/router/match-middleware/cache-lookup.ts +159 -285
  215. package/src/router/match-middleware/cache-store.ts +64 -52
  216. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  217. package/src/router/match-middleware/segment-resolution.ts +0 -22
  218. package/src/router/match-pipelines.ts +1 -42
  219. package/src/router/match-result.ts +44 -74
  220. package/src/router/metrics.ts +0 -34
  221. package/src/router/middleware-types.ts +7 -134
  222. package/src/router/middleware.ts +247 -166
  223. package/src/router/navigation-snapshot.ts +0 -51
  224. package/src/router/params-util.ts +23 -0
  225. package/src/router/pattern-matching.ts +85 -94
  226. package/src/router/prefetch-cache-ttl.ts +51 -0
  227. package/src/router/prerender-match.ts +104 -65
  228. package/src/router/preview-match.ts +3 -1
  229. package/src/router/request-classification.ts +28 -62
  230. package/src/router/revalidation.ts +123 -73
  231. package/src/router/route-snapshot.ts +0 -1
  232. package/src/router/router-context.ts +3 -28
  233. package/src/router/router-interfaces.ts +83 -35
  234. package/src/router/router-options.ts +136 -5
  235. package/src/router/router-registry.ts +2 -5
  236. package/src/router/segment-resolution/fresh.ts +97 -84
  237. package/src/router/segment-resolution/helpers.ts +86 -6
  238. package/src/router/segment-resolution/loader-cache.ts +76 -39
  239. package/src/router/segment-resolution/revalidation.ts +272 -320
  240. package/src/router/segment-resolution/static-store.ts +19 -5
  241. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  242. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  243. package/src/router/segment-resolution.ts +5 -1
  244. package/src/router/segment-wrappers.ts +6 -5
  245. package/src/router/state-cookie-name.ts +33 -0
  246. package/src/router/substitute-pattern-params.ts +56 -0
  247. package/src/router/telemetry-otel.ts +161 -199
  248. package/src/router/telemetry.ts +96 -19
  249. package/src/router/timeout.ts +0 -20
  250. package/src/router/tracing.ts +206 -0
  251. package/src/router/trie-matching.ts +162 -64
  252. package/src/router/types.ts +9 -63
  253. package/src/router/url-params.ts +0 -5
  254. package/src/router.ts +110 -55
  255. package/src/rsc/handler-context.ts +3 -2
  256. package/src/rsc/handler.ts +264 -220
  257. package/src/rsc/helpers.ts +100 -6
  258. package/src/rsc/index.ts +2 -5
  259. package/src/rsc/json-route-result.ts +38 -0
  260. package/src/rsc/loader-fetch.ts +114 -38
  261. package/src/rsc/manifest-init.ts +28 -41
  262. package/src/rsc/origin-guard.ts +39 -25
  263. package/src/rsc/progressive-enhancement.ts +117 -11
  264. package/src/rsc/redirect-guard.ts +99 -0
  265. package/src/rsc/response-cache-serve.ts +238 -0
  266. package/src/rsc/response-error.ts +79 -12
  267. package/src/rsc/response-route-handler.ts +88 -188
  268. package/src/rsc/rsc-rendering.ts +98 -76
  269. package/src/rsc/runtime-warnings.ts +23 -10
  270. package/src/rsc/server-action.ts +281 -117
  271. package/src/rsc/ssr-setup.ts +16 -0
  272. package/src/rsc/transition-gate.ts +89 -0
  273. package/src/rsc/types.ts +23 -5
  274. package/src/runtime-env.ts +18 -0
  275. package/src/search-params.ts +35 -30
  276. package/src/segment-loader-promise.ts +31 -4
  277. package/src/segment-system.tsx +254 -143
  278. package/src/serialize.ts +243 -0
  279. package/src/server/context.ts +163 -51
  280. package/src/server/cookie-parse.ts +32 -0
  281. package/src/server/cookie-store.ts +80 -5
  282. package/src/server/handle-store.ts +21 -38
  283. package/src/server/loader-registry.ts +33 -42
  284. package/src/server/request-context.ts +287 -178
  285. package/src/ssr/index.tsx +21 -16
  286. package/src/static-handler.ts +10 -13
  287. package/src/testing/cache-status.ts +162 -0
  288. package/src/testing/collect-handle.ts +40 -0
  289. package/src/testing/dispatch.ts +701 -0
  290. package/src/testing/dom.entry.ts +22 -0
  291. package/src/testing/e2e/fixture.ts +188 -0
  292. package/src/testing/e2e/index.ts +128 -0
  293. package/src/testing/e2e/matchers.ts +35 -0
  294. package/src/testing/e2e/page-helpers.ts +272 -0
  295. package/src/testing/e2e/parity.ts +387 -0
  296. package/src/testing/e2e/server.ts +195 -0
  297. package/src/testing/flight-matchers.ts +97 -0
  298. package/src/testing/flight-normalize.ts +11 -0
  299. package/src/testing/flight-runtime.d.ts +57 -0
  300. package/src/testing/flight-tree.ts +682 -0
  301. package/src/testing/flight.entry.ts +52 -0
  302. package/src/testing/flight.ts +257 -0
  303. package/src/testing/generated-routes.ts +183 -0
  304. package/src/testing/index.ts +105 -0
  305. package/src/testing/internal/context.ts +371 -0
  306. package/src/testing/internal/flight-client-globals.ts +30 -0
  307. package/src/testing/internal/seed-vars.ts +54 -0
  308. package/src/testing/render-handler.ts +357 -0
  309. package/src/testing/render-route.tsx +581 -0
  310. package/src/testing/run-loader.ts +385 -0
  311. package/src/testing/run-middleware.ts +205 -0
  312. package/src/testing/run-transition-when.ts +164 -0
  313. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  314. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  315. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  316. package/src/testing/vitest-stubs/version.ts +5 -0
  317. package/src/testing/vitest.ts +305 -0
  318. package/src/theme/ThemeProvider.tsx +20 -58
  319. package/src/theme/ThemeScript.tsx +7 -9
  320. package/src/theme/constants.ts +52 -13
  321. package/src/theme/index.ts +0 -7
  322. package/src/theme/theme-context.ts +1 -5
  323. package/src/theme/theme-script.ts +22 -21
  324. package/src/theme/use-theme.ts +0 -3
  325. package/src/types/boundaries.ts +0 -35
  326. package/src/types/cache-types.ts +13 -4
  327. package/src/types/error-types.ts +30 -90
  328. package/src/types/global-namespace.ts +54 -41
  329. package/src/types/handler-context.ts +110 -62
  330. package/src/types/index.ts +3 -10
  331. package/src/types/loader-types.ts +11 -9
  332. package/src/types/request-scope.ts +112 -0
  333. package/src/types/route-config.ts +6 -50
  334. package/src/types/route-entry.ts +0 -6
  335. package/src/types/segments.ts +135 -14
  336. package/src/urls/include-helper.ts +9 -56
  337. package/src/urls/index.ts +1 -11
  338. package/src/urls/path-helper-types.ts +29 -12
  339. package/src/urls/path-helper.ts +17 -106
  340. package/src/urls/pattern-types.ts +36 -19
  341. package/src/urls/response-types.ts +22 -29
  342. package/src/urls/type-extraction.ts +58 -139
  343. package/src/urls/urls-function.ts +1 -19
  344. package/src/use-loader.tsx +292 -107
  345. package/src/vite/debug.ts +185 -0
  346. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  347. package/src/vite/discovery/discover-routers.ts +126 -85
  348. package/src/vite/discovery/discovery-errors.ts +194 -0
  349. package/src/vite/discovery/gate-state.ts +171 -0
  350. package/src/vite/discovery/prerender-collection.ts +96 -68
  351. package/src/vite/discovery/route-types-writer.ts +40 -84
  352. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  353. package/src/vite/discovery/state.ts +44 -0
  354. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  355. package/src/vite/index.ts +2 -0
  356. package/src/vite/inject-client-debug.ts +36 -0
  357. package/src/vite/plugin-types.ts +126 -8
  358. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  359. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  360. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  361. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  362. package/src/vite/plugins/expose-action-id.ts +48 -95
  363. package/src/vite/plugins/expose-id-utils.ts +88 -55
  364. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  365. package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
  366. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  367. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  368. package/src/vite/plugins/expose-internal-ids.ts +505 -486
  369. package/src/vite/plugins/performance-tracks.ts +26 -25
  370. package/src/vite/plugins/refresh-cmd.ts +1 -1
  371. package/src/vite/plugins/use-cache-transform.ts +73 -83
  372. package/src/vite/plugins/version-injector.ts +40 -29
  373. package/src/vite/plugins/version-plugin.ts +37 -40
  374. package/src/vite/plugins/virtual-entries.ts +39 -25
  375. package/src/vite/rango.ts +109 -118
  376. package/src/vite/router-discovery.ts +718 -119
  377. package/src/vite/utils/ast-handler-extract.ts +26 -35
  378. package/src/vite/utils/banner.ts +1 -1
  379. package/src/vite/utils/bundle-analysis.ts +10 -15
  380. package/src/vite/utils/client-chunks.ts +184 -0
  381. package/src/vite/utils/directive-prologue.ts +40 -0
  382. package/src/vite/utils/forward-user-plugins.ts +171 -0
  383. package/src/vite/utils/manifest-utils.ts +4 -59
  384. package/src/vite/utils/package-resolution.ts +20 -52
  385. package/src/vite/utils/prerender-utils.ts +54 -39
  386. package/src/vite/utils/shared-utils.ts +90 -41
  387. package/src/browser/action-response-classifier.ts +0 -99
  388. package/src/browser/react/use-client-cache.ts +0 -58
  389. package/src/browser/shallow.ts +0 -40
  390. package/src/handles/index.ts +0 -7
  391. package/src/network-error-thrower.tsx +0 -23
  392. 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
  */
@@ -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,15 +107,6 @@ 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 {
@@ -112,21 +118,12 @@ export function createPartialUpdater(
112
118
  getVersion = () => undefined,
113
119
  } = config;
114
120
 
115
- /**
116
- * Get current page's cached segments as an array
117
- */
118
121
  function getCurrentCachedSegments(): ResolvedSegment[] {
119
122
  const currentKey = store.getHistoryKey();
120
123
  const cached = store.getCachedSegments(currentKey);
121
124
  return cached?.segments || [];
122
125
  }
123
126
 
124
- /**
125
- * Fetch partial update and trigger UI update
126
- *
127
- * @param tx - Transaction for committing segment state (required)
128
- * @param signal - AbortSignal to check if navigation is stale (not for aborting fetch)
129
- */
130
127
  async function fetchPartialUpdate(
131
128
  targetUrl: string,
132
129
  segmentIds: string[] | undefined,
@@ -138,26 +135,16 @@ export function createPartialUpdater(
138
135
  const segmentState = store.getSegmentState();
139
136
  const url = targetUrl || window.location.href;
140
137
 
141
- // Capture history key at start for stale revalidation consistency check
142
138
  const historyKeyAtStart = store.getHistoryKey();
143
139
 
144
- // Derive interceptSourceUrl from modes that carry it
145
- const interceptSourceUrl =
146
- mode.type === "stale-revalidation" ||
147
- mode.type === "action" ||
148
- mode.type === "navigate"
149
- ? mode.interceptSourceUrl
150
- : undefined;
140
+ const interceptSourceUrl = mode.interceptSourceUrl;
151
141
 
152
- // When leaving intercept, filter out intercept-specific segments
153
142
  let segments: string[];
154
143
  if (mode.type === "leave-intercept") {
155
144
  const currentSegments = segmentIds ?? segmentState.currentSegmentIds;
156
145
  const currentCached = getCurrentCachedSegments();
157
146
  const interceptIds = new Set(
158
- currentCached
159
- .filter((s) => s.namespace?.startsWith("intercept:"))
160
- .map((s) => s.id),
147
+ currentCached.filter(isInterceptSegment).map((s) => s.id),
161
148
  );
162
149
  segments = currentSegments.filter((id) => !interceptIds.has(id));
163
150
  debugLog(
@@ -167,12 +154,6 @@ export function createPartialUpdater(
167
154
  segments = segmentIds ?? segmentState.currentSegmentIds;
168
155
  }
169
156
 
170
- // For intercept revalidation, use the intercept source URL as previousUrl.
171
- // For leave-intercept, tx.currentUrl captures window.location.href at tx
172
- // creation, which on popstate is already the destination URL and would
173
- // tell the server "from == to". segmentState.currentUrl still points at
174
- // the URL the cached segments render (the intercept URL), which is the
175
- // correct "from" for the server's diff computation.
176
157
  const previousUrl =
177
158
  mode.type === "leave-intercept"
178
159
  ? segmentState.currentUrl || tx.currentUrl
@@ -186,66 +167,76 @@ export function createPartialUpdater(
186
167
  debugLog(`[Browser] Intercept context from: ${interceptSourceUrl}`);
187
168
  }
188
169
 
189
- // Get cached segments for merging with server diff.
190
- // When navigating with targetCacheSegments, use those for consistency.
191
- // Otherwise fall back to current page's segments (for same-route revalidation).
192
170
  const targetCache =
193
- mode.type === "navigate" ? mode.targetCacheSegments : undefined;
194
- const cachedSegs =
195
- targetCache && targetCache.length > 0
196
- ? targetCache
197
- : getCurrentCachedSegments();
198
- const cachedSegsSource =
199
- 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";
200
176
  debugLog(
201
177
  `[Browser] cachedSegs source: ${cachedSegsSource} (${cachedSegs.length} segments: ${cachedSegs.map((s) => s.id).join(", ")})`,
202
178
  );
203
179
 
204
- // Fetch partial payload (no abort signal - RSC doesn't support it well)
205
180
  let fetchResult: Awaited<ReturnType<NavigationClient["fetchPartial"]>>;
206
181
  fetchResult = await client.fetchPartial({
207
182
  targetUrl: url,
208
183
  segmentIds: segments,
209
184
  previousUrl,
210
- // Mark stale when explicitly requested OR when no segments are sent
211
- // (action redirect sends empty segments for a fresh render).
212
185
  staleRevalidation:
213
186
  mode.type === "stale-revalidation" || segments.length === 0,
214
187
  version: getVersion(),
215
188
  routerId: store.getRouterId?.(),
216
189
  });
217
- // Mark navigation as streaming (response received, now parsing RSC).
218
- // Called after fetchPartial so pendingUrl stays set during the network wait,
219
- // allowing useLinkStatus to show per-link pending indicators.
220
190
  const streamingToken = tx.startStreaming();
221
- const { payload, streamComplete: rawStreamComplete } = fetchResult;
191
+ const {
192
+ payload,
193
+ streamComplete: rawStreamComplete,
194
+ fullyPrefetched,
195
+ } = fetchResult;
222
196
  debugLog("payload.metadata", payload.metadata);
223
197
 
224
- const streamComplete = rawStreamComplete.then(() => {
225
- streamingToken.end();
226
- });
227
-
228
- // Detect app switch: if routerId changed, the navigation crossed into
229
- // a different router (e.g., via host router path mount). Downgrade
230
- // partial to full so the entire tree is replaced without reconciliation
231
- // against stale segments from the previous app.
232
- if (payload.metadata?.routerId) {
233
- const prevRouterId = store.getRouterId?.();
234
- if (prevRouterId && prevRouterId !== payload.metadata.routerId) {
235
- debugLog(
236
- `[Browser] App switch detected (${prevRouterId} ${payload.metadata.routerId}), forcing full update`,
237
- );
238
- payload.metadata.isPartial = false;
239
- }
240
- store.setRouterId?.(payload.metadata.routerId);
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;
241
215
  }
242
216
 
243
- // Handle server-side redirect with state
244
217
  if (payload.metadata?.redirect) {
245
218
  if (signal?.aborted) {
246
219
  debugLog("[Browser] Ignoring stale redirect (aborted)");
247
220
  return;
248
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
+ }
249
240
  const redirectUrl = validateRedirectOrigin(
250
241
  payload.metadata.redirect.url,
251
242
  window.location.origin,
@@ -270,7 +261,6 @@ export function createPartialUpdater(
270
261
  debugLog(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
271
262
  debugLog(`[Browser] Diff: ${diff?.join(", ")}`);
272
263
 
273
- // If diff is empty, nothing changed on server side.
274
264
  if (!diff || diff.length === 0) {
275
265
  const matchedIds = matched || [];
276
266
  const cacheMap = new Map(cachedSegs.map((s) => [s.id, s]));
@@ -278,8 +268,7 @@ export function createPartialUpdater(
278
268
  .map((id: string) => cacheMap.get(id))
279
269
  .filter(Boolean) as ResolvedSegment[];
280
270
 
281
- // When navigating with cached segments to a different route, render them.
282
- if (mode.type === "navigate" && targetCache && targetCache.length > 0) {
271
+ if (mode.type === "navigate" && targetCache) {
283
272
  debugLog(
284
273
  "[Browser] No diff but navigating with cached segments - rendering target route",
285
274
  );
@@ -293,10 +282,6 @@ export function createPartialUpdater(
293
282
  existingSegments,
294
283
  );
295
284
 
296
- // tx.commit() cached the source page's handleData because
297
- // eventController hasn't been updated yet. Overwrite with the
298
- // correct cached handleData to prevent cache corruption on
299
- // subsequent navigations to this same URL.
300
285
  if (mode.targetCacheHandleData) {
301
286
  store.updateCacheHandleData(
302
287
  store.getHistoryKey(),
@@ -304,10 +289,6 @@ export function createPartialUpdater(
304
289
  );
305
290
  }
306
291
 
307
- // Include cachedHandleData in metadata so NavigationProvider can restore
308
- // breadcrumbs and other handle data from cache.
309
- // Remove `handles` from metadata to prevent NavigationProvider from
310
- // processing an empty handles stream, which would clear the cached breadcrumbs.
311
292
  const { handles: _unusedHandles, ...metadataWithoutHandles } =
312
293
  payload.metadata!;
313
294
  const cachedUpdate = {
@@ -319,10 +300,7 @@ export function createPartialUpdater(
319
300
  scroll: toScrollPayload(commitScroll),
320
301
  };
321
302
 
322
- const cachedHasTransition = existingSegments.some(
323
- (s) => s.transition,
324
- );
325
- if (cachedHasTransition) {
303
+ if (shouldStartViewTransition(existingSegments)) {
326
304
  startTransition(() => {
327
305
  if (addTransitionType) {
328
306
  addTransitionType("navigation");
@@ -337,7 +315,6 @@ export function createPartialUpdater(
337
315
  return;
338
316
  }
339
317
 
340
- // When leaving intercept, force re-render even with empty diff
341
318
  if (mode.type === "leave-intercept") {
342
319
  debugLog(
343
320
  "[Browser] Leaving intercept - forcing re-render to remove modal",
@@ -362,7 +339,6 @@ export function createPartialUpdater(
362
339
  return;
363
340
  }
364
341
 
365
- // Same route revalidation with no changes - skip UI update
366
342
  debugLog(
367
343
  "[Browser] No changes - all revalidations returned false, keeping existing UI",
368
344
  );
@@ -371,7 +347,6 @@ export function createPartialUpdater(
371
347
  return;
372
348
  }
373
349
 
374
- // Reconcile server segments with cached segments (single source of truth)
375
350
  const matchedIds = matched || [];
376
351
  const actor: ReconcileActor =
377
352
  mode.type === "stale-revalidation" || mode.type === "action"
@@ -387,7 +362,6 @@ export function createPartialUpdater(
387
362
  insertMissingDiff: true,
388
363
  });
389
364
 
390
- // HMR RESILIENCE: Check if we're missing any matched segments
391
365
  const reconciledIdSet = new Set(reconciled.segments.map((s) => s.id));
392
366
  const missingIds = matchedIds.filter(
393
367
  (id: string) => !reconciledIdSet.has(id),
@@ -415,7 +389,6 @@ export function createPartialUpdater(
415
389
  `[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`,
416
390
  );
417
391
 
418
- // Refetch with empty segments = server sends everything
419
392
  return fetchPartialUpdate(url, [], true, signal, tx, mode);
420
393
  }
421
394
 
@@ -424,44 +397,59 @@ export function createPartialUpdater(
424
397
  return;
425
398
  }
426
399
 
427
- // Rebuild tree on client (await for loader data resolution)
428
400
  const renderOptions = {
429
401
  isAction: mode.type === "action",
430
- forceAwait: mode.type === "stale-revalidation",
402
+ // forceAwait unwraps the ROUTER loader promises during render so they
403
+ // land without a loading()/fallback frame. A fully-prefetched nav has
404
+ // its router data already resolved (the prefetch stream drained), so
405
+ // awaiting it here is free and lets us commit NORMALLY (not in a
406
+ // transition) below — a normal commit still shows fallbacks for any
407
+ // CLIENT component that suspends on mount, which a transition would
408
+ // wrongly suppress by holding the old UI until that suspense settles.
409
+ forceAwait: mode.type === "stale-revalidation" || fullyPrefetched,
431
410
  interceptSegments:
432
411
  reconciled.interceptSegments.length > 0
433
412
  ? reconciled.interceptSegments
434
413
  : undefined,
435
414
  };
436
- const newTree = await (signal
437
- ? Promise.race([
415
+ let newTree: Awaited<ReturnType<typeof renderSegments>>;
416
+ if (signal) {
417
+ // Race render against abort. Store the abort handler and register it
418
+ // { once:true } so a non-aborted render (which wins the race) can
419
+ // remove it in finally — otherwise the listener stays attached and the
420
+ // rejecting promise never settles. Mirrors teeWithCompletion in
421
+ // browser/response-adapter.ts.
422
+ let onAbort: (() => void) | undefined;
423
+ const abortPromise = new Promise<never>((_, reject) => {
424
+ if (signal.aborted) {
425
+ reject(new DOMException("Navigation aborted", "AbortError"));
426
+ return;
427
+ }
428
+ onAbort = () =>
429
+ reject(new DOMException("Navigation aborted", "AbortError"));
430
+ signal.addEventListener("abort", onAbort, { once: true });
431
+ });
432
+ try {
433
+ newTree = await Promise.race([
438
434
  renderSegments(reconciled.mainSegments, renderOptions),
439
- new Promise<never>((_, reject) => {
440
- if (signal.aborted) {
441
- reject(new DOMException("Navigation aborted", "AbortError"));
442
- }
443
- signal.addEventListener("abort", () => {
444
- reject(new DOMException("Navigation aborted", "AbortError"));
445
- });
446
- }),
447
- ])
448
- : renderSegments(reconciled.mainSegments, renderOptions));
449
-
450
- // Final abort check before committing - another navigation may have started
435
+ abortPromise,
436
+ ]);
437
+ } finally {
438
+ if (onAbort) signal.removeEventListener("abort", onAbort);
439
+ }
440
+ } else {
441
+ newTree = await renderSegments(reconciled.mainSegments, renderOptions);
442
+ }
443
+
451
444
  if (signal?.aborted) {
452
445
  debugLog("[Browser] Ignoring stale navigation (aborted before commit)");
453
446
  return;
454
447
  }
455
448
 
456
- // Check if this is an intercept response (any slot is active)
457
449
  const isInterceptResponse = hasActiveInterceptSlots(
458
450
  payload.metadata?.slots,
459
451
  );
460
452
 
461
- // Track intercept context (only on navigation, not actions or stale revalidation)
462
- // Use the authoritative source from mode/history state when restoring an
463
- // intercept via popstate cache miss; fall back to the current URL for fresh
464
- // intercept navigations.
465
453
  const effectiveInterceptSource =
466
454
  interceptSourceUrl || segmentState.currentUrl;
467
455
  if (mode.type !== "action" && mode.type !== "stale-revalidation") {
@@ -472,9 +460,6 @@ export function createPartialUpdater(
472
460
  }
473
461
  }
474
462
 
475
- // Commit navigation - use server's matched as the authoritative segment ID list.
476
- // reconciled.segments may be missing IDs (e.g., loader segments not in diff or cache)
477
- // but the server's matched always includes all expected segment IDs.
478
463
  const allSegmentIds = matchedIds;
479
464
  const serverLocationState = payload.metadata?.locationState;
480
465
  const overrides: CommitOverrides | undefined = isInterceptResponse
@@ -493,7 +478,6 @@ export function createPartialUpdater(
493
478
  overrides,
494
479
  );
495
480
 
496
- // For stale revalidation: verify history key hasn't changed before updating UI
497
481
  if (mode.type === "stale-revalidation") {
498
482
  const historyKeyNow = store.getHistoryKey();
499
483
  if (historyKeyNow !== historyKeyAtStart) {
@@ -506,9 +490,21 @@ export function createPartialUpdater(
506
490
 
507
491
  debugLog("[partial-update] updating document");
508
492
 
509
- // Emit update to trigger React render.
510
- // Scroll info is included so NavigationProvider applies it after React commits.
511
- const hasTransition = reconciled.mainSegments.some((s) => s.transition);
493
+ const hasTransition = shouldStartViewTransition(reconciled.segments);
494
+ // TEMP DIAGNOSTIC (transition.when hold investigation) - remove before merge.
495
+ // Prints which reconciled segment still carries a transition after the
496
+ // server-side when-gate, and whether the commit will be held in a
497
+ // startTransition. If `withTransition` lists an ancestor (layout/root)
498
+ // id rather than the gated leaf, an ungated ancestor transition is the
499
+ // cause of the missing loading() fallback.
500
+ console.log("[VT-DIAG]", {
501
+ mode: mode.type,
502
+ hasTransition,
503
+ withTransition: reconciled.segments
504
+ .filter((s) => s.transition)
505
+ .map((s) => s.id),
506
+ all: reconciled.segments.map((s) => s.id),
507
+ });
512
508
  const scrollPayload = toScrollPayload(navScroll);
513
509
 
514
510
  if (mode.type === "action" || mode.type === "stale-revalidation") {
@@ -534,6 +530,20 @@ export function createPartialUpdater(
534
530
  });
535
531
  });
536
532
  } else {
533
+ // Normal commit (cold/partial nav AND fully-prefetched nav). For a
534
+ // fully-prefetched nav, renderOptions.forceAwait (above) unwrapped the
535
+ // already-resolved ROUTER loader data AND route content during render, so
536
+ // the new tree carries it inline with no loading()/fallback frame — yet we
537
+ // still commit NORMALLY here rather than in a transition. A transition
538
+ // holds the OLD UI until ALL suspense in the new tree settles, including a
539
+ // CLIENT component that starts its own data request only when mounted
540
+ // (post-commit) under a persistent boundary; that would retain the
541
+ // previous page indefinitely with no feedback. A normal commit lets such
542
+ // client-initiated suspense reveal a fallback (correct) while the router
543
+ // data — genuinely ready — never flashes. Cold/partial navs
544
+ // (fullyPrefetched=false) do not forceAwait, so they stream their
545
+ // fallbacks. Explicit transition() routes keep the broader content-hold
546
+ // via the hasTransition branch above (the documented opt-in).
537
547
  onUpdate({
538
548
  root: newTree,
539
549
  metadata: payload.metadata!,
@@ -544,7 +554,6 @@ export function createPartialUpdater(
544
554
  debugLog("[Browser] Navigation complete");
545
555
  return;
546
556
  } else {
547
- // Full update (fallback)
548
557
  console.warn(`[Browser] Full update (fallback)`);
549
558
 
550
559
  const segments = payload.metadata?.segments || [];
@@ -570,13 +579,23 @@ export function createPartialUpdater(
570
579
  })
571
580
  : tx.commit(segmentIds, segments);
572
581
 
573
- const fullHasTransition = segments.some(
574
- (s: ResolvedSegment) => s.transition,
575
- );
582
+ const fullHasTransition = shouldStartViewTransition(segments);
576
583
  const fullScrollPayload = toScrollPayload(fullScroll);
577
584
 
578
585
  if (mode.type === "stale-revalidation") {
579
586
  await rawStreamComplete;
587
+ // Mirror the partial branch's history-key staleness guard (above): the
588
+ // await above is a real async suspension, so the user may have navigated
589
+ // away while this background revalidation was draining. Dropping a late
590
+ // full-update here prevents it from clobbering the freshly committed UI
591
+ // of the page the user moved to.
592
+ const historyKeyNow = store.getHistoryKey();
593
+ if (historyKeyNow !== historyKeyAtStart) {
594
+ debugLog(
595
+ `[Browser] Stale revalidation (full update): history key changed (${historyKeyAtStart} -> ${historyKeyNow}), skipping UI update`,
596
+ );
597
+ return;
598
+ }
580
599
  startTransition(() => {
581
600
  if (fullHasTransition && addTransitionType) {
582
601
  addTransitionType("action");
@@ -588,7 +607,7 @@ export function createPartialUpdater(
588
607
  });
589
608
  });
590
609
  } else if (mode.type === "action") {
591
- startTransition(async () => {
610
+ startTransition(() => {
592
611
  if (fullHasTransition && addTransitionType) {
593
612
  addTransitionType("action");
594
613
  }