@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.141

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 (460) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +432 -7
  3. package/dist/bin/rango.js +2073 -213
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +7258 -2714
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +140 -67
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +329 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +487 -0
  12. package/skills/caching/SKILL.md +357 -25
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +246 -0
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +16 -10
  19. package/skills/document-cache/SKILL.md +87 -62
  20. package/skills/fonts/SKILL.md +6 -4
  21. package/skills/handler-use/SKILL.md +364 -0
  22. package/skills/hooks/SKILL.md +557 -79
  23. package/skills/host-router/SKILL.md +320 -0
  24. package/skills/i18n/SKILL.md +276 -0
  25. package/skills/intercept/SKILL.md +207 -15
  26. package/skills/layout/SKILL.md +146 -6
  27. package/skills/links/SKILL.md +304 -25
  28. package/skills/loader/SKILL.md +616 -54
  29. package/skills/middleware/SKILL.md +217 -37
  30. package/skills/migrate-nextjs/SKILL.md +611 -0
  31. package/skills/migrate-react-router/SKILL.md +927 -0
  32. package/skills/mime-routes/SKILL.md +42 -11
  33. package/skills/observability/SKILL.md +194 -0
  34. package/skills/parallel/SKILL.md +284 -3
  35. package/skills/ppr/SKILL.md +293 -0
  36. package/skills/prerender/SKILL.md +437 -52
  37. package/skills/rango/SKILL.md +369 -22
  38. package/skills/react-compiler/SKILL.md +168 -0
  39. package/skills/response-routes/SKILL.md +263 -121
  40. package/skills/route/SKILL.md +350 -21
  41. package/skills/router-setup/SKILL.md +246 -33
  42. package/skills/scripts/SKILL.md +179 -0
  43. package/skills/server-actions/SKILL.md +775 -0
  44. package/skills/shell-manifest/SKILL.md +185 -0
  45. package/skills/streams-and-websockets/SKILL.md +283 -0
  46. package/skills/tailwind/SKILL.md +27 -3
  47. package/skills/testing/SKILL.md +126 -222
  48. package/skills/testing/bindings.md +103 -0
  49. package/skills/testing/cache-prerender.md +127 -0
  50. package/skills/testing/client-components.md +124 -0
  51. package/skills/testing/e2e-parity.md +125 -0
  52. package/skills/testing/flight.md +91 -0
  53. package/skills/testing/handles.md +131 -0
  54. package/skills/testing/loader.md +128 -0
  55. package/skills/testing/middleware.md +99 -0
  56. package/skills/testing/render-handler.md +122 -0
  57. package/skills/testing/response-routes.md +95 -0
  58. package/skills/testing/reverse-and-types.md +85 -0
  59. package/skills/testing/server-actions.md +107 -0
  60. package/skills/testing/server-tree.md +128 -0
  61. package/skills/testing/setup.md +123 -0
  62. package/skills/theme/SKILL.md +9 -8
  63. package/skills/typesafety/SKILL.md +532 -103
  64. package/skills/use-cache/SKILL.md +367 -0
  65. package/skills/vercel/SKILL.md +128 -0
  66. package/skills/view-transitions/SKILL.md +337 -0
  67. package/src/__augment-tests__/augment.ts +81 -0
  68. package/src/__augment-tests__/augmented.check.ts +116 -0
  69. package/src/__internal.ts +77 -44
  70. package/src/bin/rango.ts +312 -15
  71. package/src/browser/action-coordinator.ts +114 -0
  72. package/src/browser/action-fence.ts +47 -0
  73. package/src/browser/app-shell.ts +39 -0
  74. package/src/browser/app-version.ts +14 -0
  75. package/src/browser/connection-warmup.ts +134 -0
  76. package/src/browser/cookie-name.ts +140 -0
  77. package/src/browser/event-controller.ts +293 -202
  78. package/src/browser/history-state.ts +101 -0
  79. package/src/browser/index.ts +3 -3
  80. package/src/browser/intercept-utils.ts +52 -0
  81. package/src/browser/invalidate-client-cache.ts +52 -0
  82. package/src/browser/link-interceptor.ts +24 -4
  83. package/src/browser/logging.ts +11 -0
  84. package/src/browser/merge-segment-loaders.ts +20 -12
  85. package/src/browser/navigation-bridge.ts +385 -576
  86. package/src/browser/navigation-client.ts +245 -75
  87. package/src/browser/navigation-store-handle.ts +38 -0
  88. package/src/browser/navigation-store.ts +184 -118
  89. package/src/browser/navigation-transaction.ts +247 -0
  90. package/src/browser/network-error-handler.ts +88 -0
  91. package/src/browser/partial-update.ts +412 -364
  92. package/src/browser/prefetch/cache.ts +359 -0
  93. package/src/browser/prefetch/fetch.ts +452 -0
  94. package/src/browser/prefetch/observer.ts +65 -0
  95. package/src/browser/prefetch/policy.ts +48 -0
  96. package/src/browser/prefetch/queue.ts +209 -0
  97. package/src/browser/prefetch/resource-ready.ts +77 -0
  98. package/src/browser/rango-state.ts +194 -0
  99. package/src/browser/react/Link.tsx +275 -68
  100. package/src/browser/react/NavigationProvider.tsx +265 -109
  101. package/src/browser/react/ScrollRestoration.tsx +10 -6
  102. package/src/browser/react/context.ts +11 -0
  103. package/src/browser/react/filter-segment-order.ts +70 -0
  104. package/src/browser/react/index.ts +0 -48
  105. package/src/browser/react/location-state-shared.ts +272 -60
  106. package/src/browser/react/location-state.ts +90 -20
  107. package/src/browser/react/mount-context.ts +6 -1
  108. package/src/browser/react/nonce-context.ts +23 -0
  109. package/src/browser/react/shallow-equal.ts +27 -0
  110. package/src/browser/react/use-action.ts +35 -66
  111. package/src/browser/react/use-handle.ts +39 -126
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +39 -13
  114. package/src/browser/react/use-navigation.ts +53 -69
  115. package/src/browser/react/use-params.ts +75 -0
  116. package/src/browser/react/use-pathname.ts +47 -0
  117. package/src/browser/react/use-reverse.ts +106 -0
  118. package/src/browser/react/use-router.ts +98 -0
  119. package/src/browser/react/use-search-params.ts +51 -0
  120. package/src/browser/react/use-segments.ts +72 -99
  121. package/src/browser/response-adapter.ts +164 -0
  122. package/src/browser/rsc-router.tsx +300 -72
  123. package/src/browser/scroll-restoration.ts +138 -50
  124. package/src/browser/segment-reconciler.ts +243 -0
  125. package/src/browser/segment-structure-assert.ts +17 -1
  126. package/src/browser/server-action-bridge.ts +668 -613
  127. package/src/browser/types.ts +223 -51
  128. package/src/browser/validate-redirect-origin.ts +56 -0
  129. package/src/build/collect-fallback-refs.ts +107 -0
  130. package/src/build/generate-manifest.ts +252 -161
  131. package/src/build/generate-route-types.ts +41 -1038
  132. package/src/build/index.ts +12 -7
  133. package/src/build/prefix-tree-utils.ts +123 -0
  134. package/src/build/route-trie.ts +225 -42
  135. package/src/build/route-types/ast-helpers.ts +25 -0
  136. package/src/build/route-types/ast-route-extraction.ts +105 -0
  137. package/src/build/route-types/codegen.ts +113 -0
  138. package/src/build/route-types/include-resolution.ts +812 -0
  139. package/src/build/route-types/param-extraction.ts +51 -0
  140. package/src/build/route-types/per-module-writer.ts +144 -0
  141. package/src/build/route-types/router-processing.ts +695 -0
  142. package/src/build/route-types/scan-filter.ts +85 -0
  143. package/src/build/route-types/source-scan.ts +216 -0
  144. package/src/build/runtime-discovery.ts +223 -0
  145. package/src/cache/background-task.ts +34 -0
  146. package/src/cache/cache-error.ts +104 -0
  147. package/src/cache/cache-key-utils.ts +60 -0
  148. package/src/cache/cache-policy.ts +199 -0
  149. package/src/cache/cache-runtime.ts +525 -0
  150. package/src/cache/cache-scope.ts +298 -332
  151. package/src/cache/cache-tag.ts +103 -0
  152. package/src/cache/cf/cf-base64.ts +33 -0
  153. package/src/cache/cf/cf-cache-constants.ts +127 -0
  154. package/src/cache/cf/cf-cache-store.ts +2508 -158
  155. package/src/cache/cf/cf-cache-types.ts +349 -0
  156. package/src/cache/cf/cf-kv-utils.ts +46 -0
  157. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  158. package/src/cache/cf/index.ts +17 -17
  159. package/src/cache/document-cache.ts +199 -92
  160. package/src/cache/handle-capture.ts +81 -0
  161. package/src/cache/handle-snapshot.ts +111 -0
  162. package/src/cache/index.ts +24 -35
  163. package/src/cache/memory-segment-store.ts +363 -30
  164. package/src/cache/profile-registry.ts +88 -0
  165. package/src/cache/read-through-swr.ts +178 -0
  166. package/src/cache/segment-codec.ts +248 -0
  167. package/src/cache/shell-snapshot.ts +368 -0
  168. package/src/cache/tag-invalidation.ts +230 -0
  169. package/src/cache/taint.ts +153 -0
  170. package/src/cache/types.ts +222 -211
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1113 -0
  173. package/src/client.rsc.tsx +43 -21
  174. package/src/client.tsx +131 -347
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +109 -0
  177. package/src/component-utils.ts +23 -4
  178. package/src/components/DefaultDocument.tsx +13 -3
  179. package/src/context-var.ts +168 -0
  180. package/src/debug.ts +19 -9
  181. package/src/decode-loader-results.ts +52 -0
  182. package/src/defer.ts +185 -0
  183. package/src/deps/ssr.ts +0 -1
  184. package/src/encode-kv.ts +49 -0
  185. package/src/errors.ts +106 -10
  186. package/src/escape-script.ts +52 -0
  187. package/src/handle.ts +110 -35
  188. package/src/handles/MetaTags.tsx +83 -59
  189. package/src/handles/Scripts.tsx +183 -0
  190. package/src/handles/breadcrumbs.ts +93 -0
  191. package/src/handles/deferred-resolution.ts +127 -0
  192. package/src/handles/is-thenable.ts +18 -0
  193. package/src/handles/meta.ts +44 -53
  194. package/src/handles/script.ts +244 -0
  195. package/src/host/cookie-handler.ts +20 -65
  196. package/src/host/errors.ts +21 -30
  197. package/src/host/index.ts +13 -9
  198. package/src/host/pattern-matcher.ts +50 -79
  199. package/src/host/router.ts +151 -121
  200. package/src/host/testing.ts +45 -32
  201. package/src/host/types.ts +52 -11
  202. package/src/host/utils.ts +2 -2
  203. package/src/href-client.ts +192 -57
  204. package/src/index.rsc.ts +173 -35
  205. package/src/index.ts +241 -73
  206. package/src/internal-debug.ts +9 -2
  207. package/src/loader-store.ts +500 -0
  208. package/src/loader.rsc.ts +31 -99
  209. package/src/loader.ts +30 -12
  210. package/src/missing-id-error.ts +68 -0
  211. package/src/outlet-context.ts +1 -1
  212. package/src/outlet-provider.tsx +41 -0
  213. package/src/prerender/param-hash.ts +16 -14
  214. package/src/prerender/store.ts +121 -21
  215. package/src/prerender.ts +460 -26
  216. package/src/redirect-origin.ts +100 -0
  217. package/src/regex-escape.ts +8 -0
  218. package/src/render-error-thrower.tsx +20 -0
  219. package/src/response-utils.ts +62 -0
  220. package/src/reverse.ts +198 -128
  221. package/src/root-error-boundary.tsx +42 -48
  222. package/src/route-content-wrapper.tsx +22 -77
  223. package/src/route-definition/dsl-helpers.ts +1116 -0
  224. package/src/route-definition/helper-factories.ts +88 -0
  225. package/src/route-definition/helpers-types.ts +505 -0
  226. package/src/route-definition/index.ts +54 -0
  227. package/src/route-definition/redirect.ts +134 -0
  228. package/src/route-definition/resolve-handler-use.ts +160 -0
  229. package/src/route-definition/use-item-types.ts +29 -0
  230. package/src/route-definition.ts +1 -1481
  231. package/src/route-map-builder.ts +82 -144
  232. package/src/route-name.ts +53 -0
  233. package/src/route-types.ts +71 -45
  234. package/src/router/basename.ts +14 -0
  235. package/src/router/content-negotiation.ts +263 -0
  236. package/src/router/debug-manifest.ts +72 -0
  237. package/src/router/error-handling.ts +54 -27
  238. package/src/router/find-match.ts +245 -0
  239. package/src/router/handler-context.ts +377 -125
  240. package/src/router/instrument.ts +350 -0
  241. package/src/router/intercept-resolution.ts +59 -28
  242. package/src/router/lazy-includes.ts +254 -0
  243. package/src/router/loader-resolution.ts +421 -157
  244. package/src/router/logging.ts +106 -6
  245. package/src/router/manifest.ts +131 -57
  246. package/src/router/match-api.ts +167 -246
  247. package/src/router/match-context.ts +4 -24
  248. package/src/router/match-handlers.ts +440 -0
  249. package/src/router/match-middleware/background-revalidation.ts +117 -93
  250. package/src/router/match-middleware/cache-lookup.ts +297 -150
  251. package/src/router/match-middleware/cache-store.ts +123 -51
  252. package/src/router/match-middleware/intercept-resolution.ts +44 -43
  253. package/src/router/match-middleware/segment-resolution.ts +64 -22
  254. package/src/router/match-pipelines.ts +11 -87
  255. package/src/router/match-result.ts +121 -50
  256. package/src/router/metrics.ts +219 -28
  257. package/src/router/middleware-types.ts +93 -0
  258. package/src/router/middleware.ts +505 -441
  259. package/src/router/navigation-snapshot.ts +133 -0
  260. package/src/router/params-util.ts +23 -0
  261. package/src/router/parse-pattern.ts +115 -0
  262. package/src/router/pattern-matching.ts +311 -142
  263. package/src/router/prefetch-cache-ttl.ts +51 -0
  264. package/src/router/prefetch-limits.ts +37 -0
  265. package/src/router/prerender-match.ts +547 -0
  266. package/src/router/preview-match.ts +102 -0
  267. package/src/router/request-classification.ts +278 -0
  268. package/src/router/revalidation.ts +203 -62
  269. package/src/router/route-snapshot.ts +246 -0
  270. package/src/router/router-context.ts +45 -48
  271. package/src/router/router-interfaces.ts +554 -0
  272. package/src/router/router-options.ts +779 -0
  273. package/src/router/router-registry.ts +21 -0
  274. package/src/router/segment-resolution/fresh.ts +772 -0
  275. package/src/router/segment-resolution/helpers.ts +348 -0
  276. package/src/router/segment-resolution/loader-cache.ts +250 -0
  277. package/src/router/segment-resolution/loader-mask.ts +44 -0
  278. package/src/router/segment-resolution/revalidation.ts +1331 -0
  279. package/src/router/segment-resolution/static-store.ts +81 -0
  280. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  281. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  282. package/src/router/segment-resolution.ts +25 -1354
  283. package/src/router/segment-wrappers.ts +292 -0
  284. package/src/router/state-cookie-name.ts +33 -0
  285. package/src/router/substitute-pattern-params.ts +75 -0
  286. package/src/router/telemetry-otel.ts +261 -0
  287. package/src/router/telemetry.ts +377 -0
  288. package/src/router/timeout.ts +128 -0
  289. package/src/router/tracing.ts +206 -0
  290. package/src/router/trie-matching.ts +240 -61
  291. package/src/router/types.ts +23 -70
  292. package/src/router/url-params.ts +57 -0
  293. package/src/router.ts +781 -2378
  294. package/src/rsc/full-payload.ts +70 -0
  295. package/src/rsc/handler-context.ts +46 -0
  296. package/src/rsc/handler.ts +905 -1142
  297. package/src/rsc/helpers.ts +275 -19
  298. package/src/rsc/index.ts +2 -25
  299. package/src/rsc/json-route-result.ts +38 -0
  300. package/src/rsc/loader-fetch.ts +305 -0
  301. package/src/rsc/manifest-init.ts +77 -0
  302. package/src/rsc/nonce.ts +14 -0
  303. package/src/rsc/origin-guard.ts +155 -0
  304. package/src/rsc/progressive-enhancement.ts +502 -0
  305. package/src/rsc/redirect-guard.ts +99 -0
  306. package/src/rsc/response-cache-serve.ts +238 -0
  307. package/src/rsc/response-error.ts +104 -0
  308. package/src/rsc/response-route-handler.ts +257 -0
  309. package/src/rsc/rsc-rendering.ts +527 -0
  310. package/src/rsc/runtime-warnings.ts +55 -0
  311. package/src/rsc/server-action.ts +522 -0
  312. package/src/rsc/shell-capture.ts +897 -0
  313. package/src/rsc/shell-serve.ts +124 -0
  314. package/src/rsc/ssr-setup.ts +144 -0
  315. package/src/rsc/transition-gate.ts +89 -0
  316. package/src/rsc/types.ts +95 -12
  317. package/src/runtime-env.ts +18 -0
  318. package/src/search-params.ts +99 -82
  319. package/src/segment-content-promise.ts +67 -0
  320. package/src/segment-loader-promise.ts +149 -0
  321. package/src/segment-system.tsx +349 -134
  322. package/src/serialize.ts +243 -0
  323. package/src/server/context.ts +459 -85
  324. package/src/server/cookie-parse.ts +32 -0
  325. package/src/server/cookie-store.ts +310 -0
  326. package/src/server/fetchable-loader-store.ts +11 -6
  327. package/src/server/handle-store.ts +123 -42
  328. package/src/server/loader-registry.ts +51 -100
  329. package/src/server/request-context.ts +848 -157
  330. package/src/server.ts +15 -8
  331. package/src/ssr/index.tsx +443 -135
  332. package/src/ssr/ssr-root.tsx +228 -0
  333. package/src/static-handler.ts +45 -18
  334. package/src/testing/cache-status.ts +162 -0
  335. package/src/testing/collect-handle.ts +46 -0
  336. package/src/testing/dispatch.ts +701 -0
  337. package/src/testing/dom.entry.ts +22 -0
  338. package/src/testing/e2e/fixture.ts +188 -0
  339. package/src/testing/e2e/index.ts +128 -0
  340. package/src/testing/e2e/matchers.ts +35 -0
  341. package/src/testing/e2e/page-helpers.ts +272 -0
  342. package/src/testing/e2e/parity.ts +387 -0
  343. package/src/testing/e2e/server.ts +195 -0
  344. package/src/testing/flight-matchers.ts +97 -0
  345. package/src/testing/flight-normalize.ts +11 -0
  346. package/src/testing/flight-runtime.d.ts +57 -0
  347. package/src/testing/flight-tree.ts +682 -0
  348. package/src/testing/flight.entry.ts +52 -0
  349. package/src/testing/flight.ts +257 -0
  350. package/src/testing/generated-routes.ts +199 -0
  351. package/src/testing/index.ts +105 -0
  352. package/src/testing/internal/context.ts +371 -0
  353. package/src/testing/internal/flight-client-globals.ts +30 -0
  354. package/src/testing/internal/seed-vars.ts +54 -0
  355. package/src/testing/render-handler.ts +357 -0
  356. package/src/testing/render-route.tsx +584 -0
  357. package/src/testing/run-loader.ts +385 -0
  358. package/src/testing/run-middleware.ts +205 -0
  359. package/src/testing/run-transition-when.ts +164 -0
  360. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  361. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  362. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  363. package/src/testing/vitest-stubs/version.ts +5 -0
  364. package/src/testing/vitest.ts +305 -0
  365. package/src/theme/ThemeProvider.tsx +76 -98
  366. package/src/theme/ThemeScript.tsx +12 -14
  367. package/src/theme/constants.ts +57 -15
  368. package/src/theme/index.ts +3 -20
  369. package/src/theme/theme-context.ts +5 -35
  370. package/src/theme/theme-script.ts +43 -39
  371. package/src/theme/use-theme.ts +0 -3
  372. package/src/types/boundaries.ts +123 -0
  373. package/src/types/cache-types.ts +207 -0
  374. package/src/types/error-types.ts +132 -0
  375. package/src/types/global-namespace.ts +113 -0
  376. package/src/types/handler-context.ts +839 -0
  377. package/src/types/index.ts +81 -0
  378. package/src/types/loader-types.ts +212 -0
  379. package/src/types/request-scope.ts +112 -0
  380. package/src/types/route-config.ts +138 -0
  381. package/src/types/route-entry.ts +114 -0
  382. package/src/types/segments.ts +271 -0
  383. package/src/types.ts +1 -1795
  384. package/src/urls/include-helper.ts +162 -0
  385. package/src/urls/include-provider.ts +71 -0
  386. package/src/urls/index.ts +44 -0
  387. package/src/urls/path-helper-types.ts +413 -0
  388. package/src/urls/path-helper.ts +280 -0
  389. package/src/urls/pattern-types.ts +160 -0
  390. package/src/urls/response-types.ts +109 -0
  391. package/src/urls/type-extraction.ts +316 -0
  392. package/src/urls/urls-function.ts +80 -0
  393. package/src/urls.ts +1 -1341
  394. package/src/use-loader.tsx +406 -141
  395. package/src/vercel/index.ts +11 -0
  396. package/src/vercel/tracing.ts +88 -0
  397. package/src/vite/debug.ts +185 -0
  398. package/src/vite/discovery/bundle-postprocess.ts +182 -0
  399. package/src/vite/discovery/discover-routers.ts +389 -0
  400. package/src/vite/discovery/discovery-errors.ts +255 -0
  401. package/src/vite/discovery/gate-state.ts +171 -0
  402. package/src/vite/discovery/prerender-collection.ts +467 -0
  403. package/src/vite/discovery/route-types-writer.ts +214 -0
  404. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  405. package/src/vite/discovery/state.ts +161 -0
  406. package/src/vite/discovery/virtual-module-codegen.ts +183 -0
  407. package/src/vite/index.ts +23 -2255
  408. package/src/vite/inject-client-debug.ts +36 -0
  409. package/src/vite/plugin-types.ts +303 -0
  410. package/src/vite/plugins/cjs-to-esm.ts +90 -0
  411. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  412. package/src/vite/plugins/client-ref-hashing.ts +118 -0
  413. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  414. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  415. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  416. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
  417. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
  418. package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
  419. package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
  420. package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
  421. package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
  422. package/src/vite/plugins/expose-ids/types.ts +45 -0
  423. package/src/vite/plugins/expose-internal-ids.ts +805 -0
  424. package/src/vite/plugins/performance-tracks.ts +89 -0
  425. package/src/vite/plugins/refresh-cmd.ts +127 -0
  426. package/src/vite/plugins/use-cache-transform.ts +313 -0
  427. package/src/vite/plugins/vercel-output.ts +384 -0
  428. package/src/vite/plugins/version-injector.ts +94 -0
  429. package/src/vite/plugins/version-plugin.ts +263 -0
  430. package/src/vite/plugins/virtual-entries.ts +234 -0
  431. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  432. package/src/vite/rango.ts +560 -0
  433. package/src/vite/router-discovery.ts +1638 -0
  434. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
  435. package/src/vite/utils/banner.ts +36 -0
  436. package/src/vite/utils/bundle-analysis.ts +132 -0
  437. package/src/vite/utils/client-chunks.ts +184 -0
  438. package/src/vite/utils/directive-prologue.ts +40 -0
  439. package/src/vite/utils/forward-user-plugins.ts +171 -0
  440. package/src/vite/utils/manifest-utils.ts +15 -0
  441. package/src/vite/utils/package-resolution.ts +89 -0
  442. package/src/vite/utils/prerender-utils.ts +249 -0
  443. package/src/vite/utils/shared-utils.ts +269 -0
  444. package/CLAUDE.md +0 -43
  445. package/dist/vite/index.named-routes.gen.ts +0 -103
  446. package/src/browser/lru-cache.ts +0 -69
  447. package/src/browser/react/use-client-cache.ts +0 -56
  448. package/src/browser/request-controller.ts +0 -164
  449. package/src/browser/shallow.ts +0 -35
  450. package/src/cache/memory-store.ts +0 -253
  451. package/src/handles/index.ts +0 -6
  452. package/src/href-context.ts +0 -33
  453. package/src/network-error-thrower.tsx +0 -21
  454. package/src/router.gen.ts +0 -6
  455. package/src/static-handler.gen.ts +0 -5
  456. package/src/urls.gen.ts +0 -8
  457. package/src/vite/expose-internal-ids.ts +0 -1167
  458. package/src/vite/package-resolution.ts +0 -125
  459. package/src/vite/virtual-entries.ts +0 -114
  460. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -8,8 +8,27 @@
8
8
  * - Supports hash link scrolling
9
9
  */
10
10
 
11
+ import { debugLog } from "./logging.js";
12
+
13
+ /**
14
+ * Defers a callback to the next animation frame.
15
+ * Falls back to setTimeout(0) in environments without requestAnimationFrame.
16
+ */
17
+ const deferToNextPaint: (fn: () => void) => void =
18
+ typeof requestAnimationFrame === "function"
19
+ ? requestAnimationFrame
20
+ : (fn) => setTimeout(fn, 0);
21
+
11
22
  const SCROLL_STORAGE_KEY = "rsc-router-scroll-positions";
12
23
 
24
+ /**
25
+ * Maximum number of scroll position entries to retain.
26
+ * When exceeded, the oldest entries (by insertion order) are evicted.
27
+ * 200 entries is well within sessionStorage limits while covering
28
+ * realistic back/forward navigation depth.
29
+ */
30
+ const MAX_SCROLL_ENTRIES = 200;
31
+
13
32
  /**
14
33
  * Interval for polling scroll restoration during streaming (ms).
15
34
  * If content is still loading and we can't scroll to saved position,
@@ -29,6 +48,13 @@ const SCROLL_POLL_TIMEOUT_MS = 5000;
29
48
  */
30
49
  let savedScrollPositions: Record<string, number> = {};
31
50
 
51
+ /**
52
+ * Tracks insertion order of scroll position keys for LRU eviction.
53
+ * Most recent entries are at the end of the array.
54
+ * When a key is updated, it is moved to the end.
55
+ */
56
+ let scrollKeyOrder: string[] = [];
57
+
32
58
  /**
33
59
  * Whether scroll restoration has been initialized
34
60
  */
@@ -37,9 +63,12 @@ let initialized = false;
37
63
  /**
38
64
  * Custom getKey function for determining scroll restoration key
39
65
  */
40
- type GetScrollKeyFunction = (
41
- location: { pathname: string; search: string; hash: string; key: string }
42
- ) => string;
66
+ type GetScrollKeyFunction = (location: {
67
+ pathname: string;
68
+ search: string;
69
+ hash: string;
70
+ key: string;
71
+ }) => string;
43
72
 
44
73
  let customGetKey: GetScrollKeyFunction | null = null;
45
74
 
@@ -99,9 +128,13 @@ export function initScrollRestoration(options?: {
99
128
  const stored = sessionStorage.getItem(SCROLL_STORAGE_KEY);
100
129
  if (stored) {
101
130
  savedScrollPositions = JSON.parse(stored);
131
+ // Rebuild key order from loaded positions.
132
+ // Exact original order is lost across page loads, but this is
133
+ // acceptable -- the important invariant is bounded size.
134
+ scrollKeyOrder = Object.keys(savedScrollPositions);
102
135
  }
103
136
  } catch (e) {
104
- // Ignore parse errors
137
+ // Ignore parse errors, start with empty state
105
138
  }
106
139
 
107
140
  // Ensure current history entry has a key
@@ -117,31 +150,87 @@ export function initScrollRestoration(options?: {
117
150
 
118
151
  window.addEventListener("pagehide", handlePageHide);
119
152
 
120
- console.log("[Scroll] Initialized, loaded positions:", Object.keys(savedScrollPositions).length);
153
+ debugLog(
154
+ "[Scroll] Initialized, loaded positions:",
155
+ Object.keys(savedScrollPositions).length,
156
+ );
121
157
 
122
158
  return () => {
159
+ cancelScrollRestorationPolling();
123
160
  window.removeEventListener("pagehide", handlePageHide);
124
161
  window.history.scrollRestoration = "auto";
125
162
  initialized = false;
163
+ savedScrollPositions = {};
164
+ scrollKeyOrder = [];
126
165
  };
127
166
  }
128
167
 
129
168
  /**
130
- * Save the current scroll position for the current history entry
169
+ * Save the current scroll position for the current history entry.
170
+ * Maintains bounded size by evicting oldest entries when the limit is exceeded.
131
171
  */
132
172
  export function saveCurrentScrollPosition(): void {
133
173
  const key = getScrollKey();
174
+
175
+ // If this key already exists, remove it from its current position
176
+ // in the order array so it can be re-appended at the end (most recent).
177
+ const existingIndex = scrollKeyOrder.indexOf(key);
178
+ if (existingIndex !== -1) {
179
+ scrollKeyOrder.splice(existingIndex, 1);
180
+ }
181
+
134
182
  savedScrollPositions[key] = window.scrollY;
183
+ scrollKeyOrder.push(key);
184
+
185
+ // Evict oldest entries if we exceed the limit
186
+ while (scrollKeyOrder.length > MAX_SCROLL_ENTRIES) {
187
+ const oldestKey = scrollKeyOrder.shift()!;
188
+ delete savedScrollPositions[oldestKey];
189
+ }
135
190
  }
136
191
 
137
192
  /**
138
- * Persist scroll positions to sessionStorage
193
+ * Persist scroll positions to sessionStorage.
194
+ * If the write fails (typically QuotaExceededError), evict the oldest ~1/4 of
195
+ * entries ONCE and retry the write a single time; if that still fails, remove
196
+ * our storage key entirely so we don't block other sessionStorage consumers.
197
+ * This is a single evict-then-retry-then-clear ladder, not a loop.
198
+ *
199
+ * Exported so that single eviction/retry/clear ladder is unit-testable directly.
200
+ * The browser drives it from the `pagehide` handler.
139
201
  */
140
- function persistToSessionStorage(): void {
202
+ export function persistToSessionStorage(): void {
141
203
  try {
142
- sessionStorage.setItem(SCROLL_STORAGE_KEY, JSON.stringify(savedScrollPositions));
204
+ sessionStorage.setItem(
205
+ SCROLL_STORAGE_KEY,
206
+ JSON.stringify(savedScrollPositions),
207
+ );
143
208
  } catch (e) {
144
- console.warn("[Scroll] Failed to persist to sessionStorage:", e);
209
+ // Likely QuotaExceededError. Evict oldest entries and retry.
210
+ const evictCount = Math.max(1, Math.floor(scrollKeyOrder.length / 4));
211
+ for (let i = 0; i < evictCount && scrollKeyOrder.length > 0; i++) {
212
+ const oldestKey = scrollKeyOrder.shift()!;
213
+ delete savedScrollPositions[oldestKey];
214
+ }
215
+
216
+ try {
217
+ sessionStorage.setItem(
218
+ SCROLL_STORAGE_KEY,
219
+ JSON.stringify(savedScrollPositions),
220
+ );
221
+ } catch (retryErr) {
222
+ // Storage still full after eviction. Clear our key entirely so we
223
+ // don't block other sessionStorage consumers.
224
+ console.warn(
225
+ "[Scroll] Failed to persist to sessionStorage after eviction, clearing scroll data:",
226
+ retryErr,
227
+ );
228
+ try {
229
+ sessionStorage.removeItem(SCROLL_STORAGE_KEY);
230
+ } catch {
231
+ // Nothing more we can do
232
+ }
233
+ }
145
234
  }
146
235
  }
147
236
 
@@ -189,50 +278,35 @@ export function restoreScrollPosition(options?: {
189
278
  return false;
190
279
  }
191
280
 
192
- // Check if page is tall enough to scroll to saved position
193
- const maxScrollY = document.documentElement.scrollHeight - window.innerHeight;
194
- const canScrollToPosition = savedY <= maxScrollY;
195
-
196
- if (canScrollToPosition) {
197
- window.scrollTo(0, savedY);
198
- console.log("[Scroll] Restored position:", savedY, "for key:", key);
199
- return true;
200
- }
201
-
202
- // Scroll as far as we can for now
203
- window.scrollTo(0, maxScrollY);
204
- console.log("[Scroll] Partial restore to:", maxScrollY, "target:", savedY);
205
-
206
- // Poll while streaming until we can scroll to target position
281
+ // If streaming, poll until streaming ends then scroll to saved position
207
282
  if (options?.retryIfStreaming && options?.isStreaming?.()) {
208
283
  const startTime = Date.now();
209
284
 
210
285
  pendingPollInterval = setInterval(() => {
211
- // Stop if we've exceeded the timeout
212
286
  if (Date.now() - startTime > SCROLL_POLL_TIMEOUT_MS) {
213
- console.log("[Scroll] Polling timeout, giving up");
287
+ debugLog("[Scroll] Polling timeout, giving up");
214
288
  cancelScrollRestorationPolling();
215
289
  return;
216
290
  }
217
291
 
218
- // Stop if streaming ended
219
292
  if (!options.isStreaming?.()) {
220
- console.log("[Scroll] Streaming ended, stopping poll");
221
- cancelScrollRestorationPolling();
222
- return;
223
- }
224
-
225
- // Check if we can now scroll to the target position
226
- const currentMaxScrollY = document.documentElement.scrollHeight - window.innerHeight;
227
- if (savedY <= currentMaxScrollY) {
228
293
  window.scrollTo(0, savedY);
229
- console.log("[Scroll] Poll restored position:", savedY);
294
+ debugLog("[Scroll] Restored after streaming:", savedY);
230
295
  cancelScrollRestorationPolling();
231
296
  }
232
297
  }, SCROLL_POLL_INTERVAL_MS);
298
+
299
+ return true;
233
300
  }
234
301
 
235
- return false;
302
+ // Not streaming — scroll after React commits and browser paints.
303
+ // startTransition defers the DOM commit, so scrolling synchronously
304
+ // would be overwritten when React replaces the content.
305
+ deferToNextPaint(() => {
306
+ window.scrollTo(0, savedY);
307
+ debugLog("[Scroll] Restored position:", savedY, "for key:", key);
308
+ });
309
+ return true;
236
310
  }
237
311
 
238
312
  /**
@@ -249,7 +323,7 @@ export function scrollToHash(): boolean {
249
323
  const element = document.getElementById(id);
250
324
  if (element) {
251
325
  element.scrollIntoView();
252
- console.log("[Scroll] Scrolled to hash element:", id);
326
+ debugLog("[Scroll] Scrolled to hash element:", id);
253
327
  return true;
254
328
  }
255
329
  } catch (e) {
@@ -263,6 +337,8 @@ export function scrollToHash(): boolean {
263
337
  * Scroll to top of page
264
338
  */
265
339
  export function scrollToTop(): void {
340
+ if (typeof window === "undefined") return;
341
+ if (typeof window.scrollTo !== "function") return;
266
342
  window.scrollTo(0, 0);
267
343
  }
268
344
 
@@ -287,31 +363,43 @@ export function handleNavigationEnd(options: {
287
363
  scroll?: boolean;
288
364
  isStreaming?: () => boolean;
289
365
  }): void {
290
- if (!initialized) {
291
- return;
292
- }
293
-
294
366
  const { restore = false, scroll = true, isStreaming } = options;
295
367
 
296
- // Don't scroll if explicitly disabled
297
- if (scroll === false) {
368
+ // Don't scroll if explicitly disabled or not in a browser
369
+ if (scroll === false || typeof window === "undefined") {
298
370
  return;
299
371
  }
300
372
 
301
- // For back/forward (restore), try to restore saved position
302
- if (restore) {
373
+ // Save/restore requires initialization (sessionStorage, history state).
374
+ // But basic scroll-to-top and hash scrolling work without it — this
375
+ // matters during cross-app navigation where ScrollRestoration unmounts
376
+ // and remounts, creating a brief window where initialized is false.
377
+ if (restore && initialized) {
303
378
  if (restoreScrollPosition({ retryIfStreaming: true, isStreaming })) {
304
379
  return;
305
380
  }
306
381
  // Fall through to hash or top if no saved position
307
382
  }
308
383
 
309
- // Try hash scrolling first
384
+ // scrollToHash / scrollToTop run synchronously here.
385
+ // handleNavigationEnd is invoked from NavigationProvider's
386
+ // useLayoutEffect (post-commit, pre-paint), so a sync scrollTo is
387
+ // captured by the upcoming paint AND by startViewTransition's snapshot.
388
+ // Deferring via rAF here pushed the call past the snapshot capture,
389
+ // making forward navigations wrapped in a layout/route view transition
390
+ // skip scroll-to-top — the live DOM scrolled but the captured snapshot
391
+ // was at the previous scroll position, so the user-facing page stayed
392
+ // visually clamped at the source page's scrollY (often the new tree's
393
+ // max scroll for tall→short navs). Y=0 / a hash element are robust
394
+ // against unmeasured layout, so sync scroll is correct here even
395
+ // before the new tree's scrollHeight settles.
396
+ //
397
+ // (The restore branch above keeps deferToNextPaint because savedY
398
+ // depends on the new tree's max scroll; sync scrollTo against an
399
+ // unmeasured DOM would clamp savedY to whatever the old/zero max was.)
310
400
  if (scrollToHash()) {
311
401
  return;
312
402
  }
313
-
314
- // Default: scroll to top
315
403
  scrollToTop();
316
404
  }
317
405
 
@@ -0,0 +1,243 @@
1
+ import type { ResolvedSegment } from "./types.js";
2
+ import {
3
+ mergeSegmentLoaders,
4
+ needsLoaderMerge,
5
+ insertMissingDiffSegments,
6
+ } from "./merge-segment-loaders.js";
7
+ import { assertSegmentStructure } from "./segment-structure-assert.js";
8
+ import { splitInterceptSegments } from "./intercept-utils.js";
9
+ import { debugLog } from "./logging.js";
10
+
11
+ /**
12
+ * Determines the merging behavior for segment reconciliation.
13
+ *
14
+ * - 'action': From server-action-bridge's own merge. Always merges loaders,
15
+ * always preserves cached loading (even undefined), never clears cached
16
+ * segment loading.
17
+ * - 'navigation': From partial-update during normal navigation. Does NOT merge
18
+ * loaders, preserves cached loading only when defined, clears truthy loading
19
+ * on cached segments not in server diff.
20
+ * - 'stale-revalidation': From partial-update during stale revalidation or
21
+ * action-triggered refetch. Merges loaders, always preserves cached loading
22
+ * (same as action), clears truthy loading on cached segments not in server diff.
23
+ */
24
+ export type ReconcileActor = "navigation" | "action" | "stale-revalidation";
25
+
26
+ export interface ReconcileInput {
27
+ actor: ReconcileActor;
28
+ /** All segment IDs the server expects the client to have (matched array) */
29
+ matched: string[];
30
+ /** Segment IDs that changed (diff array) */
31
+ diff: string[];
32
+ /** Segments returned from server (raw array, keyed internally by ID) */
33
+ serverSegments: ResolvedSegment[];
34
+ /** Cached segments from current page (raw array, keyed internally by ID) */
35
+ cachedSegments: ResolvedSegment[];
36
+ /** When true, diff segments not in matched are inserted after their parent
37
+ * layout. Used during navigation when consolidation fetch returns loader
38
+ * segments that aren't in the matched array. */
39
+ insertMissingDiff?: boolean;
40
+ }
41
+
42
+ export interface ReconcileResult {
43
+ /** All merged segments in matched order (for caching and committing) */
44
+ segments: ResolvedSegment[];
45
+ /** Main segments excluding intercepts (for rendering) */
46
+ mainSegments: ResolvedSegment[];
47
+ /** Intercept segments only (passed via render options) */
48
+ interceptSegments: ResolvedSegment[];
49
+ }
50
+
51
+ /**
52
+ * Single source of truth for merging server segments with cached segments.
53
+ *
54
+ * Replaces the duplicated merge loops in server-action-bridge.ts and
55
+ * partial-update.ts. The actor parameter controls the subtle behavioral
56
+ * differences between action and navigation merging:
57
+ *
58
+ * Loading preservation:
59
+ * - action/stale-revalidation: Always preserves cached loading value when it
60
+ * differs from server (even when cached is undefined). This prevents tree
61
+ * structure changes that would remount components and destroy useActionState
62
+ * during action revalidation or action-triggered refetch.
63
+ * - navigation: Preserves cached loading only when the cached value is defined
64
+ * (not undefined). When cached is undefined, lets server value through
65
+ * because we're building a new tree.
66
+ *
67
+ * Loader merging:
68
+ * - action/stale-revalidation: Merges partial loader data when server returns
69
+ * fewer loaders than cached (revalidation only updated some loaders).
70
+ * - navigation: Does not merge (full navigation fetches complete data).
71
+ *
72
+ * Cached segment handling (segments in matched but not in server response):
73
+ * - action: Returns cached segment as-is (preserve tree structure).
74
+ * - navigation/stale-revalidation: Clears truthy loading to undefined
75
+ * (prevents showing stale skeletons), but preserves loading=false
76
+ * (suppressed boundary is structural).
77
+ */
78
+ export function reconcileSegments(input: ReconcileInput): ReconcileResult {
79
+ const { actor, matched, diff, insertMissingDiff } = input;
80
+ const shouldMergeLoaders = actor !== "navigation";
81
+ const context = actor === "action" ? "action-bridge" : "partial-update";
82
+
83
+ // Build lookup maps from arrays
84
+ const serverSegments = new Map<string, ResolvedSegment>();
85
+ input.serverSegments.forEach((s) => serverSegments.set(s.id, s));
86
+ const cachedSegments = new Map<string, ResolvedSegment>();
87
+ input.cachedSegments.forEach((s) => cachedSegments.set(s.id, s));
88
+
89
+ const diffSet = new Set(diff);
90
+ debugLog(
91
+ `[reconcile] actor=${actor}, matched=${matched.length}, diff=${diff.length}`,
92
+ );
93
+ debugLog(
94
+ `[reconcile] server segments: ${[...serverSegments.keys()].join(", ")}`,
95
+ );
96
+ debugLog(
97
+ `[reconcile] cached segments: ${[...cachedSegments.keys()].join(", ")}`,
98
+ );
99
+
100
+ const segments = matched
101
+ .map((segId: string) => {
102
+ const fromServer = serverSegments.get(segId);
103
+ const fromCache = cachedSegments.get(segId);
104
+
105
+ if (fromServer) {
106
+ const inDiff = diffSet.has(segId);
107
+ // Merge partial loader data when server returns fewer loaders than cached
108
+ if (shouldMergeLoaders && needsLoaderMerge(fromServer, fromCache)) {
109
+ debugLog(
110
+ `[reconcile] ${segId}: MERGE loaders (server partial, ${inDiff ? "in diff" : "not in diff"})`,
111
+ );
112
+ return mergeSegmentLoaders(fromServer, fromCache);
113
+ }
114
+
115
+ // Preserve cached structural properties to maintain consistent React tree.
116
+ // Changing these between renders alters the element nesting
117
+ // (with/without RouteContentWrapper, MountContextProvider, etc.),
118
+ // causing React to remount components and destroy useActionState.
119
+ if (fromCache) {
120
+ let merged = fromServer;
121
+
122
+ // When server returns component: null for a layout segment, it means
123
+ // "this segment doesn't need re-rendering" - preserve the cached component
124
+ // to maintain the outlet chain and prevent React tree changes
125
+ if (
126
+ fromServer.component === null &&
127
+ fromServer.type === "layout" &&
128
+ fromCache.component != null
129
+ ) {
130
+ merged = { ...merged, component: fromCache.component };
131
+ }
132
+
133
+ // Loading preservation is actor-aware:
134
+ // - action/stale-revalidation: always preserve cached value to prevent
135
+ // tree remount (even when cached is undefined, to avoid adding a
136
+ // Suspense boundary that wasn't there before)
137
+ // - navigation: only when cached is defined (building a new tree)
138
+ if (actor !== "navigation") {
139
+ if (fromServer.loading !== fromCache.loading) {
140
+ merged = { ...merged, loading: fromCache.loading };
141
+ }
142
+ } else {
143
+ if (
144
+ fromCache.loading !== undefined &&
145
+ fromServer.loading !== fromCache.loading
146
+ ) {
147
+ merged = { ...merged, loading: fromCache.loading };
148
+ }
149
+ }
150
+
151
+ // mountPath: SSR segments may lack mountPath while revalidated segments
152
+ // include it. The conditional MountContextProvider wrapper changes tree depth.
153
+ if (fromServer.mountPath !== fromCache.mountPath) {
154
+ merged = { ...merged, mountPath: fromCache.mountPath };
155
+ }
156
+
157
+ // Dev-mode assertion: warn if the merged result still differs from cache
158
+ // in tree-structural properties. This catches bugs where the merge code
159
+ // above fails to preserve a value it should have.
160
+ assertSegmentStructure(fromCache, merged, context);
161
+
162
+ debugLog(
163
+ `[reconcile] ${segId}: SERVER+CACHE merge (${inDiff ? "in diff" : "not in diff"}, type=${fromServer.type}, component=${fromServer.component === null ? "null→cached" : "server"})`,
164
+ );
165
+ return merged;
166
+ }
167
+ debugLog(
168
+ `[reconcile] ${segId}: SERVER only (${inDiff ? "in diff" : "not in diff"}, type=${fromServer.type}, no cache entry)`,
169
+ );
170
+ return fromServer;
171
+ }
172
+
173
+ // Fall back to cached segment (server expects client to already have it)
174
+ if (!fromCache) {
175
+ if (actor === "action") {
176
+ console.error(`[Browser] MISSING SEGMENT: ${segId} not in cache!`);
177
+ } else {
178
+ console.warn(`[Browser] Missing segment: ${segId}`);
179
+ }
180
+ return fromCache;
181
+ }
182
+
183
+ debugLog(
184
+ `[reconcile] ${segId}: CACHE only (not from server, type=${fromCache.type}, component=${fromCache.component != null ? "yes" : "null"})`,
185
+ );
186
+
187
+ // Return the cached segment as-is, regardless of actor. We used to clear
188
+ // truthy `loading` here to prevent a stale Suspense fallback from
189
+ // committing against cached content, but that swapped the render tree
190
+ // from the LoaderBoundary branch to the plain OutletProvider branch
191
+ // inside renderSegments, causing React to unmount the entire chain
192
+ // (LoaderBoundary > Suspense > LoaderResolver > RouteContentWrapper >
193
+ // Suspender) every time the user opened an intercept or navigated back
194
+ // to a cached page. The flicker is now prevented by renderSegments'
195
+ // promise memoization keeping React's use() in "known fulfilled" state,
196
+ // so preserving `loading` keeps the element tree stable.
197
+ return fromCache;
198
+ })
199
+ .filter(Boolean) as ResolvedSegment[];
200
+
201
+ // Insert diff segments not in matched (e.g., loader segments from consolidation fetch).
202
+ // Only needed during navigation - action bridge doesn't use this.
203
+ if (insertMissingDiff) {
204
+ const matchedIdSet = new Set(matched);
205
+ insertMissingDiffSegments(segments, diff, matchedIdSet, serverSegments);
206
+ }
207
+
208
+ const { main, intercept } = splitInterceptSegments(segments);
209
+
210
+ return {
211
+ segments,
212
+ mainSegments: main,
213
+ interceptSegments: intercept,
214
+ };
215
+ }
216
+
217
+ /**
218
+ * Reconcile error segments with cached segments.
219
+ *
220
+ * For error responses, the server returns the error boundary segment.
221
+ * This function overlays error segments onto the full cached tree,
222
+ * preserving sibling layouts that aren't in the error parent chain.
223
+ */
224
+ export function reconcileErrorSegments(
225
+ cachedSegments: ResolvedSegment[],
226
+ errorSegments: ResolvedSegment[],
227
+ ): ReconcileResult {
228
+ const errorMap = new Map<string, ResolvedSegment>();
229
+ errorSegments.forEach((s) => errorMap.set(s.id, s));
230
+
231
+ const segments = cachedSegments.map((cached) => {
232
+ const fromServer = errorMap.get(cached.id);
233
+ return fromServer || cached;
234
+ });
235
+
236
+ const { main, intercept } = splitInterceptSegments(segments);
237
+
238
+ return {
239
+ segments,
240
+ mainSegments: main,
241
+ interceptSegments: intercept,
242
+ };
243
+ }
@@ -48,7 +48,7 @@ export function assertSegmentStructure(
48
48
 
49
49
  if (cachedCategory !== incomingCategory) {
50
50
  console.warn(
51
- `[RSC Router] Tree structure mismatch detected in ${context} ` +
51
+ `[Rango] Tree structure mismatch detected in ${context} ` +
52
52
  `for segment "${cached.id}": loading category changed from ` +
53
53
  `"${cachedCategory}" (${describeLoading(cached.loading)}) to ` +
54
54
  `"${incomingCategory}" (${describeLoading(incoming.loading)}). ` +
@@ -57,6 +57,22 @@ export function assertSegmentStructure(
57
57
  `The merge code should preserve the cached loading value.`,
58
58
  );
59
59
  }
60
+
61
+ // Check mountPath consistency. MountContextProvider is conditionally added
62
+ // in renderSegments() when mountPath is truthy, changing tree depth.
63
+ const cachedHasMount = !!cached.mountPath;
64
+ const incomingHasMount = !!incoming.mountPath;
65
+ if (cachedHasMount !== incomingHasMount) {
66
+ console.warn(
67
+ `[Rango] MountContextProvider mismatch detected in ${context} ` +
68
+ `for segment "${cached.id}": mountPath changed from ` +
69
+ `${cachedHasMount ? `"${cached.mountPath}"` : "undefined"} to ` +
70
+ `${incomingHasMount ? `"${incoming.mountPath}"` : "undefined"}. ` +
71
+ `This will cause React to remount the component, destroying ` +
72
+ `useActionState and other client state. ` +
73
+ `The merge code should preserve the cached mountPath value.`,
74
+ );
75
+ }
60
76
  }
61
77
 
62
78
  function describeLoading(loading: unknown): string {