@rangojs/router 0.0.0-experimental.9c9afef3 → 0.0.0-experimental.a014d2b7

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