@rangojs/router 0.0.0-experimental.135c6902 → 0.0.0-experimental.136

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