@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bdaf10aa

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 (449) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +303 -741
  3. package/dist/bin/rango.js +730 -184
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +4344 -1335
  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 +86 -15
  9. package/skills/api-client/SKILL.md +211 -0
  10. package/skills/breadcrumbs/SKILL.md +85 -6
  11. package/skills/bundle-analysis/SKILL.md +159 -0
  12. package/skills/cache-guide/SKILL.md +251 -24
  13. package/skills/caching/SKILL.md +375 -17
  14. package/skills/catalog.json +271 -0
  15. package/skills/comparison/SKILL.md +50 -0
  16. package/skills/comparison/agents/openai.yaml +4 -0
  17. package/skills/comparison/references/framework-comparison.md +837 -0
  18. package/skills/composability/SKILL.md +110 -4
  19. package/skills/css/SKILL.md +76 -0
  20. package/skills/debug-manifest/SKILL.md +5 -3
  21. package/skills/defer-hydration/SKILL.md +235 -0
  22. package/skills/document-cache/SKILL.md +87 -56
  23. package/skills/fonts/SKILL.md +1 -1
  24. package/skills/handler-use/SKILL.md +364 -0
  25. package/skills/hooks/SKILL.md +73 -691
  26. package/skills/hooks/data.md +273 -0
  27. package/skills/hooks/handle-and-actions.md +103 -0
  28. package/skills/hooks/navigation.md +110 -0
  29. package/skills/hooks/outlets.md +41 -0
  30. package/skills/hooks/state.md +228 -0
  31. package/skills/hooks/urls.md +135 -0
  32. package/skills/host-router/SKILL.md +129 -27
  33. package/skills/i18n/SKILL.md +276 -0
  34. package/skills/intercept/SKILL.md +94 -18
  35. package/skills/layout/SKILL.md +62 -19
  36. package/skills/links/SKILL.md +249 -17
  37. package/skills/loader/SKILL.md +302 -54
  38. package/skills/middleware/SKILL.md +59 -16
  39. package/skills/migrate-nextjs/SKILL.md +745 -0
  40. package/skills/migrate-react-router/SKILL.md +153 -0
  41. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  42. package/skills/migrate-react-router/component-migration.md +196 -0
  43. package/skills/migrate-react-router/data-and-actions.md +225 -0
  44. package/skills/migrate-react-router/route-mapping.md +271 -0
  45. package/skills/mime-routes/SKILL.md +29 -2
  46. package/skills/observability/SKILL.md +202 -0
  47. package/skills/parallel/SKILL.md +225 -10
  48. package/skills/ppr/SKILL.md +622 -0
  49. package/skills/prerender/SKILL.md +178 -124
  50. package/skills/rango/SKILL.md +318 -24
  51. package/skills/react-compiler/SKILL.md +168 -0
  52. package/skills/response-routes/SKILL.md +138 -49
  53. package/skills/route/SKILL.md +172 -9
  54. package/skills/router-setup/SKILL.md +131 -11
  55. package/skills/scripts/SKILL.md +179 -0
  56. package/skills/server-actions/SKILL.md +776 -0
  57. package/skills/shell-manifest/SKILL.md +185 -0
  58. package/skills/streams-and-websockets/SKILL.md +283 -0
  59. package/skills/tailwind/SKILL.md +28 -4
  60. package/skills/testing/SKILL.md +130 -0
  61. package/skills/testing/bindings.md +103 -0
  62. package/skills/testing/cache-prerender.md +127 -0
  63. package/skills/testing/client-components.md +124 -0
  64. package/skills/testing/e2e-parity.md +125 -0
  65. package/skills/testing/flight.md +91 -0
  66. package/skills/testing/handles.md +131 -0
  67. package/skills/testing/loader.md +128 -0
  68. package/skills/testing/middleware.md +99 -0
  69. package/skills/testing/render-handler.md +122 -0
  70. package/skills/testing/response-routes.md +95 -0
  71. package/skills/testing/reverse-and-types.md +85 -0
  72. package/skills/testing/server-actions.md +107 -0
  73. package/skills/testing/server-tree.md +128 -0
  74. package/skills/testing/setup.md +123 -0
  75. package/skills/theme/SKILL.md +1 -1
  76. package/skills/typesafety/SKILL.md +45 -616
  77. package/skills/typesafety/env-and-bindings.md +254 -0
  78. package/skills/typesafety/generated-files-and-cli.md +335 -0
  79. package/skills/typesafety/params-and-search.md +153 -0
  80. package/skills/typesafety/route-types.md +209 -0
  81. package/skills/use-cache/SKILL.md +74 -15
  82. package/skills/vercel/SKILL.md +128 -0
  83. package/skills/view-transitions/SKILL.md +337 -0
  84. package/src/__augment-tests__/augment.ts +81 -0
  85. package/src/__augment-tests__/augmented.check.ts +116 -0
  86. package/src/__internal.ts +1 -66
  87. package/src/browser/action-coordinator.ts +53 -36
  88. package/src/browser/action-fence.ts +47 -0
  89. package/src/browser/app-shell.ts +39 -0
  90. package/src/browser/app-version.ts +14 -0
  91. package/src/browser/connection-warmup.ts +134 -0
  92. package/src/browser/cookie-name.ts +140 -0
  93. package/src/browser/event-controller.ts +257 -158
  94. package/src/browser/history-state.ts +21 -0
  95. package/src/browser/index.ts +3 -3
  96. package/src/browser/invalidate-client-cache.ts +52 -0
  97. package/src/browser/logging.ts +28 -0
  98. package/src/browser/merge-segment-loaders.ts +6 -4
  99. package/src/browser/navigation-bridge.ts +132 -33
  100. package/src/browser/navigation-client.ts +218 -68
  101. package/src/browser/navigation-store-handle.ts +38 -0
  102. package/src/browser/navigation-store.ts +203 -80
  103. package/src/browser/navigation-transaction.ts +18 -66
  104. package/src/browser/network-error-handler.ts +34 -7
  105. package/src/browser/partial-update.ts +241 -127
  106. package/src/browser/prefetch/cache.ts +271 -44
  107. package/src/browser/prefetch/fetch.ts +367 -40
  108. package/src/browser/prefetch/queue.ts +144 -23
  109. package/src/browser/prefetch/resource-ready.ts +77 -0
  110. package/src/browser/rango-state.ts +158 -76
  111. package/src/browser/react/Link.tsx +121 -16
  112. package/src/browser/react/NavigationProvider.tsx +240 -122
  113. package/src/browser/react/ScrollRestoration.tsx +10 -6
  114. package/src/browser/react/context.ts +7 -2
  115. package/src/browser/react/filter-segment-order.ts +66 -7
  116. package/src/browser/react/index.ts +0 -48
  117. package/src/browser/react/location-state-shared.ts +178 -8
  118. package/src/browser/react/location-state.ts +39 -14
  119. package/src/browser/react/use-action.ts +6 -15
  120. package/src/browser/react/use-handle.ts +23 -69
  121. package/src/browser/react/use-href.tsx +8 -1
  122. package/src/browser/react/use-link-status.ts +33 -8
  123. package/src/browser/react/use-navigation.ts +32 -7
  124. package/src/browser/react/use-params.ts +20 -10
  125. package/src/browser/react/use-reverse.ts +106 -0
  126. package/src/browser/react/use-router.ts +46 -11
  127. package/src/browser/react/use-search-params.ts +0 -5
  128. package/src/browser/react/use-segments.ts +11 -21
  129. package/src/browser/response-adapter.ts +99 -8
  130. package/src/browser/rsc-router.tsx +272 -80
  131. package/src/browser/scroll-restoration.ts +56 -22
  132. package/src/browser/segment-reconciler.ts +44 -7
  133. package/src/browser/segment-structure-assert.ts +2 -2
  134. package/src/browser/server-action-bridge.ts +244 -71
  135. package/src/browser/types.ts +136 -12
  136. package/src/browser/validate-redirect-origin.ts +43 -16
  137. package/src/build/collect-fallback-refs.ts +107 -0
  138. package/src/build/generate-manifest.ts +207 -158
  139. package/src/build/generate-route-types.ts +6 -1
  140. package/src/build/index.ts +11 -3
  141. package/src/build/prefix-tree-utils.ts +123 -0
  142. package/src/build/route-trie.ts +198 -41
  143. package/src/build/route-types/ast-route-extraction.ts +15 -8
  144. package/src/build/route-types/codegen.ts +16 -5
  145. package/src/build/route-types/include-resolution.ts +464 -63
  146. package/src/build/route-types/param-extraction.ts +6 -3
  147. package/src/build/route-types/per-module-writer.ts +22 -6
  148. package/src/build/route-types/router-processing.ts +336 -110
  149. package/src/build/route-types/scan-filter.ts +9 -2
  150. package/src/build/route-types/source-scan.ts +216 -0
  151. package/src/build/runtime-discovery.ts +13 -21
  152. package/src/cache/cache-error.ts +104 -0
  153. package/src/cache/cache-key-utils.ts +58 -13
  154. package/src/cache/cache-policy.ts +108 -34
  155. package/src/cache/cache-runtime.ts +454 -97
  156. package/src/cache/cache-scope.ts +235 -103
  157. package/src/cache/cache-tag.ts +149 -0
  158. package/src/cache/cf/cf-base64.ts +33 -0
  159. package/src/cache/cf/cf-cache-constants.ts +127 -0
  160. package/src/cache/cf/cf-cache-store.ts +2446 -170
  161. package/src/cache/cf/cf-cache-types.ts +349 -0
  162. package/src/cache/cf/cf-kv-utils.ts +46 -0
  163. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  164. package/src/cache/cf/index.ts +11 -17
  165. package/src/cache/document-cache.ts +144 -49
  166. package/src/cache/handle-snapshot.ts +70 -0
  167. package/src/cache/index.ts +24 -20
  168. package/src/cache/memory-segment-store.ts +243 -37
  169. package/src/cache/profile-registry.ts +46 -31
  170. package/src/cache/read-through-swr.ts +56 -12
  171. package/src/cache/segment-codec.ts +13 -21
  172. package/src/cache/shell-snapshot.ts +417 -0
  173. package/src/cache/tag-invalidation.ts +230 -0
  174. package/src/cache/taint.ts +55 -0
  175. package/src/cache/types.ts +194 -99
  176. package/src/cache/vercel/index.ts +11 -0
  177. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  178. package/src/client.rsc.tsx +41 -21
  179. package/src/client.tsx +116 -290
  180. package/src/cloudflare/index.ts +11 -0
  181. package/src/cloudflare/tracing.ts +108 -0
  182. package/src/component-utils.ts +19 -0
  183. package/src/components/DefaultDocument.tsx +8 -2
  184. package/src/context-var.ts +84 -2
  185. package/src/debug.ts +2 -2
  186. package/src/decode-loader-results.ts +52 -0
  187. package/src/defer.ts +185 -0
  188. package/src/deps/ssr.ts +0 -1
  189. package/src/encode-kv.ts +49 -0
  190. package/src/errors.ts +30 -4
  191. package/src/escape-script.ts +52 -0
  192. package/src/handle.ts +104 -34
  193. package/src/handles/MetaTags.tsx +24 -53
  194. package/src/handles/Scripts.tsx +183 -0
  195. package/src/handles/breadcrumbs.ts +35 -8
  196. package/src/handles/deferred-resolution.ts +127 -0
  197. package/src/handles/is-thenable.ts +18 -0
  198. package/src/handles/meta.ts +14 -40
  199. package/src/handles/script.ts +244 -0
  200. package/src/host/cookie-handler.ts +9 -60
  201. package/src/host/errors.ts +13 -22
  202. package/src/host/index.ts +9 -2
  203. package/src/host/pattern-matcher.ts +23 -52
  204. package/src/host/router.ts +107 -99
  205. package/src/host/testing.ts +40 -27
  206. package/src/host/types.ts +37 -4
  207. package/src/host/utils.ts +1 -1
  208. package/src/href-client.ts +137 -22
  209. package/src/index.rsc.ts +100 -13
  210. package/src/index.ts +143 -19
  211. package/src/internal-debug.ts +11 -10
  212. package/src/loader-store.ts +500 -0
  213. package/src/loader.rsc.ts +20 -13
  214. package/src/loader.ts +12 -11
  215. package/src/missing-id-error.ts +68 -0
  216. package/src/outlet-context.ts +1 -1
  217. package/src/outlet-provider.tsx +1 -5
  218. package/src/prerender/param-hash.ts +16 -16
  219. package/src/prerender/store.ts +37 -41
  220. package/src/prerender.ts +215 -86
  221. package/src/redirect-origin.ts +114 -0
  222. package/src/regex-escape.ts +8 -0
  223. package/src/render-error-thrower.tsx +20 -0
  224. package/src/response-utils.ts +62 -0
  225. package/src/reverse.ts +65 -15
  226. package/src/root-error-boundary.tsx +1 -19
  227. package/src/route-content-wrapper.tsx +19 -77
  228. package/src/route-definition/dsl-helpers.ts +485 -303
  229. package/src/route-definition/helper-factories.ts +28 -140
  230. package/src/route-definition/helpers-types.ts +153 -77
  231. package/src/route-definition/index.ts +4 -2
  232. package/src/route-definition/redirect.ts +53 -12
  233. package/src/route-definition/resolve-handler-use.ts +160 -0
  234. package/src/route-definition/use-item-types.ts +29 -0
  235. package/src/route-map-builder.ts +48 -21
  236. package/src/route-types.ts +37 -46
  237. package/src/router/basename.ts +14 -0
  238. package/src/router/content-negotiation.ts +164 -17
  239. package/src/router/error-handling.ts +45 -18
  240. package/src/router/find-match.ts +130 -29
  241. package/src/router/handler-context.ts +83 -39
  242. package/src/router/instrument.ts +355 -0
  243. package/src/router/intercept-resolution.ts +50 -24
  244. package/src/router/lazy-includes.ts +89 -63
  245. package/src/router/loader-resolution.ts +286 -56
  246. package/src/router/logging.ts +5 -8
  247. package/src/router/manifest.ts +105 -56
  248. package/src/router/match-api.ts +178 -218
  249. package/src/router/match-context.ts +0 -22
  250. package/src/router/match-handlers.ts +211 -165
  251. package/src/router/match-middleware/background-revalidation.ts +66 -22
  252. package/src/router/match-middleware/cache-lookup.ts +214 -263
  253. package/src/router/match-middleware/cache-store.ts +105 -50
  254. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  255. package/src/router/match-middleware/segment-resolution.ts +52 -18
  256. package/src/router/match-pipelines.ts +1 -42
  257. package/src/router/match-result.ts +128 -44
  258. package/src/router/metrics.ts +5 -34
  259. package/src/router/middleware-types.ts +13 -142
  260. package/src/router/middleware.ts +301 -177
  261. package/src/router/navigation-snapshot.ts +133 -0
  262. package/src/router/params-util.ts +23 -0
  263. package/src/router/parse-pattern.ts +115 -0
  264. package/src/router/pattern-matching.ts +181 -150
  265. package/src/router/prefetch-cache-ttl.ts +51 -0
  266. package/src/router/prefetch-limits.ts +37 -0
  267. package/src/router/prerender-match.ts +203 -58
  268. package/src/router/preview-match.ts +35 -103
  269. package/src/router/request-classification.ts +291 -0
  270. package/src/router/revalidation.ts +123 -73
  271. package/src/router/route-snapshot.ts +256 -0
  272. package/src/router/router-context.ts +11 -29
  273. package/src/router/router-interfaces.ts +146 -35
  274. package/src/router/router-options.ts +202 -15
  275. package/src/router/router-registry.ts +2 -5
  276. package/src/router/segment-resolution/fresh.ts +301 -78
  277. package/src/router/segment-resolution/helpers.ts +115 -30
  278. package/src/router/segment-resolution/loader-cache.ts +156 -39
  279. package/src/router/segment-resolution/loader-mask.ts +60 -0
  280. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  281. package/src/router/segment-resolution/mask-nested.ts +83 -0
  282. package/src/router/segment-resolution/revalidation.ts +477 -385
  283. package/src/router/segment-resolution/static-store.ts +19 -5
  284. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  285. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  286. package/src/router/segment-resolution.ts +5 -1
  287. package/src/router/segment-wrappers.ts +8 -5
  288. package/src/router/state-cookie-name.ts +33 -0
  289. package/src/router/substitute-pattern-params.ts +75 -0
  290. package/src/router/telemetry-otel.ts +160 -200
  291. package/src/router/telemetry.ts +105 -20
  292. package/src/router/timeout.ts +0 -20
  293. package/src/router/tracing.ts +215 -0
  294. package/src/router/trie-matching.ts +171 -59
  295. package/src/router/types.ts +10 -63
  296. package/src/router/url-params.ts +57 -0
  297. package/src/router.ts +210 -71
  298. package/src/rsc/full-payload.ts +70 -0
  299. package/src/rsc/handler-context.ts +3 -2
  300. package/src/rsc/handler.ts +682 -508
  301. package/src/rsc/helpers.ts +168 -46
  302. package/src/rsc/index.ts +2 -5
  303. package/src/rsc/json-route-result.ts +38 -0
  304. package/src/rsc/loader-fetch.ts +127 -31
  305. package/src/rsc/manifest-init.ts +33 -42
  306. package/src/rsc/nonce.ts +10 -1
  307. package/src/rsc/origin-guard.ts +39 -25
  308. package/src/rsc/progressive-enhancement.ts +138 -15
  309. package/src/rsc/redirect-guard.ts +100 -0
  310. package/src/rsc/response-cache-serve.ts +238 -0
  311. package/src/rsc/response-error.ts +79 -12
  312. package/src/rsc/response-route-handler.ts +99 -189
  313. package/src/rsc/rsc-rendering.ts +509 -73
  314. package/src/rsc/runtime-warnings.ts +23 -10
  315. package/src/rsc/server-action.ts +287 -113
  316. package/src/rsc/shell-capture.ts +1190 -0
  317. package/src/rsc/shell-serve.ts +181 -0
  318. package/src/rsc/ssr-setup.ts +18 -2
  319. package/src/rsc/transition-gate.ts +89 -0
  320. package/src/rsc/types.ts +62 -6
  321. package/src/runtime-env.ts +18 -0
  322. package/src/search-params.ts +35 -30
  323. package/src/segment-content-promise.ts +67 -0
  324. package/src/segment-loader-promise.ts +167 -0
  325. package/src/segment-system.tsx +449 -132
  326. package/src/serialize.ts +243 -0
  327. package/src/server/context.ts +367 -61
  328. package/src/server/cookie-parse.ts +32 -0
  329. package/src/server/cookie-store.ts +152 -5
  330. package/src/server/handle-store.ts +40 -38
  331. package/src/server/loader-registry.ts +38 -46
  332. package/src/server/request-context.ts +558 -173
  333. package/src/ssr/index.tsx +491 -174
  334. package/src/ssr/inject-rsc-eager.ts +167 -0
  335. package/src/ssr/ssr-root.tsx +228 -0
  336. package/src/static-handler.ts +27 -18
  337. package/src/testing/cache-status.ts +162 -0
  338. package/src/testing/collect-handle.ts +46 -0
  339. package/src/testing/dispatch.ts +813 -0
  340. package/src/testing/dom.entry.ts +22 -0
  341. package/src/testing/e2e/fixture.ts +188 -0
  342. package/src/testing/e2e/index.ts +128 -0
  343. package/src/testing/e2e/matchers.ts +35 -0
  344. package/src/testing/e2e/page-helpers.ts +272 -0
  345. package/src/testing/e2e/parity.ts +387 -0
  346. package/src/testing/e2e/server.ts +195 -0
  347. package/src/testing/flight-matchers.ts +97 -0
  348. package/src/testing/flight-normalize.ts +11 -0
  349. package/src/testing/flight-runtime.d.ts +57 -0
  350. package/src/testing/flight-tree.ts +682 -0
  351. package/src/testing/flight.entry.ts +52 -0
  352. package/src/testing/flight.ts +257 -0
  353. package/src/testing/generated-routes.ts +199 -0
  354. package/src/testing/index.ts +105 -0
  355. package/src/testing/internal/context.ts +371 -0
  356. package/src/testing/internal/flight-client-globals.ts +30 -0
  357. package/src/testing/internal/seed-vars.ts +54 -0
  358. package/src/testing/render-handler.ts +357 -0
  359. package/src/testing/render-route.tsx +584 -0
  360. package/src/testing/run-loader.ts +385 -0
  361. package/src/testing/run-middleware.ts +205 -0
  362. package/src/testing/run-transition-when.ts +164 -0
  363. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  364. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  365. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  366. package/src/testing/vitest-stubs/version.ts +5 -0
  367. package/src/testing/vitest.ts +305 -0
  368. package/src/theme/ThemeProvider.tsx +56 -84
  369. package/src/theme/ThemeScript.tsx +7 -9
  370. package/src/theme/constants.ts +52 -13
  371. package/src/theme/index.ts +0 -7
  372. package/src/theme/theme-context.ts +1 -5
  373. package/src/theme/theme-script.ts +22 -21
  374. package/src/theme/use-theme.ts +0 -3
  375. package/src/types/boundaries.ts +0 -35
  376. package/src/types/cache-types.ts +17 -8
  377. package/src/types/error-types.ts +30 -90
  378. package/src/types/global-namespace.ts +54 -41
  379. package/src/types/handler-context.ts +234 -82
  380. package/src/types/index.ts +3 -10
  381. package/src/types/loader-types.ts +44 -15
  382. package/src/types/request-scope.ts +112 -0
  383. package/src/types/route-config.ts +20 -52
  384. package/src/types/route-entry.ts +19 -7
  385. package/src/types/segments.ts +137 -14
  386. package/src/urls/include-helper.ts +40 -75
  387. package/src/urls/include-provider.ts +71 -0
  388. package/src/urls/index.ts +2 -11
  389. package/src/urls/path-helper-types.ts +102 -23
  390. package/src/urls/path-helper.ts +62 -111
  391. package/src/urls/pattern-types.ts +84 -19
  392. package/src/urls/response-types.ts +25 -22
  393. package/src/urls/type-extraction.ts +98 -154
  394. package/src/urls/urls-function.ts +1 -19
  395. package/src/use-loader.tsx +346 -89
  396. package/src/vercel/index.ts +11 -0
  397. package/src/vercel/tracing.ts +88 -0
  398. package/src/vite/debug.ts +185 -0
  399. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  400. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  401. package/src/vite/discovery/discover-routers.ts +130 -85
  402. package/src/vite/discovery/discovery-errors.ts +255 -0
  403. package/src/vite/discovery/gate-state.ts +171 -0
  404. package/src/vite/discovery/prerender-collection.ts +214 -132
  405. package/src/vite/discovery/route-types-writer.ts +40 -84
  406. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  407. package/src/vite/discovery/state.ts +57 -6
  408. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  409. package/src/vite/index.ts +15 -0
  410. package/src/vite/inject-client-debug.ts +88 -0
  411. package/src/vite/plugin-types.ts +234 -62
  412. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  413. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  414. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  415. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  416. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  417. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  418. package/src/vite/plugins/expose-action-id.ts +49 -98
  419. package/src/vite/plugins/expose-id-utils.ts +96 -51
  420. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  421. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  422. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  423. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  424. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  425. package/src/vite/plugins/performance-tracks.ts +89 -0
  426. package/src/vite/plugins/refresh-cmd.ts +89 -27
  427. package/src/vite/plugins/use-cache-transform.ts +73 -83
  428. package/src/vite/plugins/vercel-output.ts +384 -0
  429. package/src/vite/plugins/version-injector.ts +40 -29
  430. package/src/vite/plugins/version-plugin.ts +46 -37
  431. package/src/vite/plugins/virtual-entries.ts +138 -27
  432. package/src/vite/rango.ts +353 -303
  433. package/src/vite/router-discovery.ts +1090 -166
  434. package/src/vite/utils/ast-handler-extract.ts +26 -35
  435. package/src/vite/utils/banner.ts +4 -4
  436. package/src/vite/utils/bundle-analysis.ts +10 -15
  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 +4 -59
  441. package/src/vite/utils/package-resolution.ts +20 -52
  442. package/src/vite/utils/prerender-utils.ts +98 -38
  443. package/src/vite/utils/shared-utils.ts +144 -44
  444. package/src/browser/action-response-classifier.ts +0 -99
  445. package/src/browser/react/use-client-cache.ts +0 -58
  446. package/src/browser/shallow.ts +0 -40
  447. package/src/handles/index.ts +0 -7
  448. package/src/network-error-thrower.tsx +0 -23
  449. package/src/router/middleware-cookies.ts +0 -55
@@ -8,6 +8,7 @@ import {
8
8
  generateHistoryKey,
9
9
  } from "./navigation-store.js";
10
10
  import { createEventController } from "./event-controller.js";
11
+ import { validateRedirectOrigin } from "./validate-redirect-origin.js";
11
12
  import { createNavigationClient } from "./navigation-client.js";
12
13
  import { createServerActionBridge } from "./server-action-bridge.js";
13
14
  import { createNavigationBridge } from "./navigation-bridge.js";
@@ -22,11 +23,17 @@ import type {
22
23
  import type { EventController } from "./event-controller.js";
23
24
  import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
24
25
  import { initRangoState } from "./rango-state.js";
26
+ import { registerNavigationStore } from "./navigation-store-handle.js";
25
27
  import { initPrefetchCache } from "./prefetch/cache.js";
28
+ import { setPrefetchConcurrency } from "./prefetch/queue.js";
29
+ import { setPrefetchDecoder } from "./prefetch/fetch.js";
30
+ import { setAppVersion } from "./app-version.js";
26
31
  import {
27
32
  isInterceptSegment,
28
33
  splitInterceptSegments,
29
34
  } from "./intercept-utils.js";
35
+ import { createAppShellRef } from "./app-shell.js";
36
+ import { bootLog, IS_BROWSER_DEBUG } from "./logging.js";
30
37
 
31
38
  // Vite HMR types are provided by vite/client
32
39
 
@@ -111,15 +118,26 @@ export interface BrowserAppContext {
111
118
  initialTheme?: Theme;
112
119
  /** Whether connection warmup is enabled */
113
120
  warmupEnabled?: boolean;
121
+ /** Whether the hydrated tree should be wrapped in React.StrictMode */
122
+ strictMode?: boolean;
114
123
  /** App version for prefetch version mismatch detection */
115
124
  version?: string;
125
+ /**
126
+ * App-shell ref, read through on each render so renderSegments and the
127
+ * NavigationProvider see rootLayout/basename/version without closing over a
128
+ * stale snapshot. Set once from the initial payload and not swapped within a
129
+ * session: a cross-app navigation is a full document load (X-RSC-Reload), so
130
+ * the target app establishes its own shell on load. Theme, warmup, and
131
+ * prefetch TTL are document-lifetime too (see AppShell).
132
+ */
133
+ appShellRef?: import("./app-shell.js").AppShellRef;
116
134
  }
117
135
 
118
136
  // Module-level state for the initialized app
119
137
  let browserAppContext: BrowserAppContext | null = null;
120
138
 
121
139
  /**
122
- * Initialize the browser app. Must be called before rendering RSCRouter.
140
+ * Initialize the browser app. Must be called before rendering Rango.
123
141
  *
124
142
  * This function:
125
143
  * - Loads the initial RSC payload from the stream
@@ -139,7 +157,8 @@ export async function initBrowserApp(
139
157
  initialTheme,
140
158
  } = options;
141
159
 
142
- // Load initial payload from SSR-injected __FLIGHT_DATA__
160
+ bootLog("initBrowserApp start");
161
+ bootLog("flight decode: awaiting initial payload from document stream");
143
162
  const initialPayload =
144
163
  await deps.createFromReadableStream<RscPayload>(rscStream);
145
164
 
@@ -153,6 +172,14 @@ export async function initBrowserApp(
153
172
  // Get initial segments and compute history key from current URL
154
173
  const initialSegments = (initialPayload.metadata?.segments ??
155
174
  []) as ResolvedSegment[];
175
+ if (IS_BROWSER_DEBUG) {
176
+ bootLog("initial payload decoded", {
177
+ version: initialPayload.metadata?.version,
178
+ routerId: initialPayload.metadata?.routerId,
179
+ segments: initialSegments.map((s) => s.id),
180
+ matched: initialPayload.metadata?.matched,
181
+ });
182
+ }
156
183
  const initialHistoryKey = generateHistoryKey(window.location.href);
157
184
 
158
185
  // Create navigation store with history-based caching
@@ -164,6 +191,18 @@ export async function initBrowserApp(
164
191
  ...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
165
192
  });
166
193
 
194
+ // Register the active store on the module-level handle and wire the
195
+ // jar-divergence observer before any getRangoState() read can detect a
196
+ // cross-tab/server rotation. There is no global store singleton, so this
197
+ // handle is the live reference.
198
+ registerNavigationStore(store);
199
+
200
+ // Seed router identity from the initial SSR payload so the first
201
+ // cross-app SPA navigation can detect the app switch.
202
+ if (initialPayload.metadata?.routerId) {
203
+ store.setRouterId?.(initialPayload.metadata.routerId);
204
+ }
205
+
167
206
  // Create event controller for reactive state management
168
207
  const eventController = createEventController({
169
208
  initialLocation: new URL(window.location.href),
@@ -179,11 +218,24 @@ export async function initBrowserApp(
179
218
  // This ensures useHandle returns correct data during hydration to avoid mismatch
180
219
  // The handles property is an async generator that yields on each push
181
220
  if (initialPayload.metadata?.handles) {
221
+ // This for-await consumes the handle generator to completion BEFORE
222
+ // hydrateRoot is called — on a streaming/PPR document the generator only
223
+ // ends when its stream side does, so the per-push logs below are the
224
+ // primary probe for "the document render is holding hydration".
225
+ bootLog("handles: consuming payload handle stream (pre-hydration await)");
182
226
  const handlesGenerator = initialPayload.metadata.handles;
183
227
  let lastHandleData: Record<string, Record<string, unknown[]>> = {};
228
+ let handlePushes = 0;
184
229
  for await (const handleData of handlesGenerator) {
185
230
  lastHandleData = handleData;
231
+ if (IS_BROWSER_DEBUG) {
232
+ handlePushes += 1;
233
+ bootLog(`handles: push #${handlePushes}`, {
234
+ segments: Object.keys(handleData),
235
+ });
236
+ }
186
237
  }
238
+ bootLog("handles: stream complete", { pushes: handlePushes });
187
239
  // Initialize event controller with initial handle state before hydration.
188
240
  eventController.setHandleData(
189
241
  lastHandleData,
@@ -193,31 +245,62 @@ export async function initBrowserApp(
193
245
  // Update the initial cache entry with the processed handleData
194
246
  // The cache entry was created by createNavigationStore but without handleData
195
247
  store.updateCacheHandleData(initialHistoryKey, lastHandleData);
248
+ } else {
249
+ bootLog("handles: none in payload");
196
250
  }
197
251
 
198
252
  // Create composable utilities
199
253
  const client = createNavigationClient(deps);
200
254
 
201
- // Extract rootLayout and version from metadata for browser-side re-renders
202
- const rootLayout = initialPayload.metadata?.rootLayout;
255
+ // Capture the per-router app-shell. rootLayout, basename, and version live
256
+ // here and are read through the ref at call time rather than closed over.
257
+ // It is set once from the initial payload and not swapped within a session:
258
+ // a cross-app navigation is a full document load (X-RSC-Reload), so the
259
+ // target app establishes its own shell on load.
203
260
  const version = initialPayload.metadata?.version;
261
+ const appShellRef = createAppShellRef({
262
+ routerId: initialPayload.metadata?.routerId,
263
+ rootLayout: initialPayload.metadata?.rootLayout,
264
+ basename: initialPayload.metadata?.basename,
265
+ version,
266
+ });
204
267
 
205
- // Initialize the localStorage state key for cache invalidation.
206
- // Uses the build version so a new deploy automatically busts all cached prefetches.
207
- initRangoState(version ?? "0");
268
+ // Initialize the rango state cookie for cache invalidation. The build version
269
+ // busts cached prefetches on deploy; the server-resolved cookie name
270
+ // namespaces the cookie so sibling apps on the same origin don't collide
271
+ // (falls back to the bare default prefix if metadata lacks the name).
272
+ initRangoState(version ?? "0", initialPayload.metadata?.stateCookieName);
273
+ setAppVersion(version);
208
274
 
209
- // Initialize the in-memory prefetch cache TTL from server config.
210
- // A value of 0 disables the cache; undefined falls back to the module default.
275
+ // Initialize the in-memory prefetch cache (TTL + max size) and the prefetch
276
+ // queue concurrency from server config. A TTL of 0 disables the cache;
277
+ // undefined values fall back to the module defaults.
211
278
  const prefetchCacheTTL = initialPayload.metadata?.prefetchCacheTTL;
212
- if (prefetchCacheTTL !== undefined) {
213
- initPrefetchCache(prefetchCacheTTL);
279
+ const prefetchCacheSize = initialPayload.metadata?.prefetchCacheSize;
280
+ if (prefetchCacheTTL !== undefined || prefetchCacheSize !== undefined) {
281
+ initPrefetchCache(prefetchCacheTTL, prefetchCacheSize);
282
+ }
283
+ const prefetchConcurrency = initialPayload.metadata?.prefetchConcurrency;
284
+ if (prefetchConcurrency !== undefined) {
285
+ setPrefetchConcurrency(prefetchConcurrency);
214
286
  }
215
287
 
216
- // Create a bound renderSegments that includes rootLayout
288
+ // Wire the RSC decoder so prefetches decode eagerly and warm the route's
289
+ // client chunks (same createFromFetch the navigation client uses).
290
+ setPrefetchDecoder((response) => deps.createFromFetch<RscPayload>(response));
291
+
292
+ // Create a bound renderSegments that reads rootLayout through the shell ref.
293
+ // The shell is set once at init and not swapped within a session (a cross-app
294
+ // navigation is a full document load), so this always renders this app's
295
+ // Document; reading through the ref just avoids closing over a stale value.
217
296
  const renderSegments = (
218
297
  segments: ResolvedSegment[],
219
298
  options?: RenderSegmentsOptions,
220
- ) => baseRenderSegments(segments, { ...options, rootLayout });
299
+ ) =>
300
+ baseRenderSegments(segments, {
301
+ ...options,
302
+ rootLayout: appShellRef.get().rootLayout,
303
+ });
221
304
 
222
305
  // Lazy reference for navigation bridge — the action bridge is created first
223
306
  // but may need to trigger SPA navigation for action redirects.
@@ -231,10 +314,15 @@ export async function initBrowserApp(
231
314
  deps,
232
315
  onUpdate: (update) => store.emitUpdate(update),
233
316
  renderSegments,
234
- version,
235
317
  onNavigate: (url, options) => {
236
318
  if (!navigateFn) {
237
- window.location.href = url;
319
+ // Navigation bridge not wired yet: hard-navigate, but re-validate
320
+ // same-origin defensively so this init-window fallback cannot become an
321
+ // open redirect (the normal path validates inside the navigation bridge).
322
+ const safe = validateRedirectOrigin(url, window.location.origin);
323
+ if (safe) {
324
+ window.location.href = safe;
325
+ }
238
326
  return Promise.resolve();
239
327
  }
240
328
  return navigateFn(url, options);
@@ -249,7 +337,7 @@ export async function initBrowserApp(
249
337
  client,
250
338
  onUpdate: (update) => store.emitUpdate(update),
251
339
  renderSegments,
252
- version,
340
+ version: version,
253
341
  });
254
342
 
255
343
  // Connect action redirect → navigation bridge (now that both are initialized)
@@ -259,79 +347,169 @@ export async function initBrowserApp(
259
347
  if (linkInterception) {
260
348
  navigationBridge.registerLinkInterception();
261
349
  }
350
+ bootLog("bridges registered (action + navigation)");
262
351
 
263
352
  // Build initial tree with rootLayout
353
+ bootLog("building initial segment tree (renderSegments)");
264
354
  const initialTree = renderSegments(initialPayload.metadata!.segments);
355
+ if (IS_BROWSER_DEBUG && initialTree instanceof Promise) {
356
+ initialTree.then(
357
+ () => bootLog("initial segment tree settled"),
358
+ (err: unknown) => bootLog("initial segment tree rejected", { err }),
359
+ );
360
+ }
265
361
 
266
- // Setup HMR
362
+ // Setup HMR with debounce — burst saves (format-on-save, rapid edits)
363
+ // fire many rsc:update events in quick succession. Without debouncing,
364
+ // each event triggers a fetchPartial() which on slow routes can pile up
365
+ // and overwhelm the worker (cross-request promise issues, 500s).
267
366
  if (import.meta.hot) {
268
- import.meta.hot.on("rsc:update", async () => {
269
- console.log("[RSCRouter] HMR: Server update, refetching RSC");
270
-
271
- const handle = eventController.startNavigation(window.location.href, {
272
- replace: true,
273
- });
274
- const streamingToken = handle.startStreaming();
275
-
276
- const interceptSourceUrl = store.getInterceptSourceUrl();
277
-
278
- try {
279
- const { payload, streamComplete } = await client.fetchPartial({
280
- targetUrl: window.location.href,
281
- segmentIds: [],
282
- previousUrl: store.getSegmentState().currentUrl,
283
- interceptSourceUrl: interceptSourceUrl || undefined,
284
- hmr: true,
367
+ let hmrTimer: ReturnType<typeof setTimeout> | null = null;
368
+ let hmrAbort: AbortController | null = null;
369
+
370
+ import.meta.hot.on("rsc:update", () => {
371
+ // Cancel any pending debounce timer
372
+ if (hmrTimer !== null) {
373
+ clearTimeout(hmrTimer);
374
+ }
375
+
376
+ // Abort any in-flight HMR fetch so it doesn't race with the next one
377
+ if (hmrAbort) {
378
+ hmrAbort.abort();
379
+ hmrAbort = null;
380
+ }
381
+
382
+ // Debounce: wait 200ms of quiet before fetching
383
+ hmrTimer = setTimeout(async () => {
384
+ hmrTimer = null;
385
+
386
+ // Don't interrupt an active user navigation — startNavigation()
387
+ // would abort it and refetch the old URL (window.location.href
388
+ // hasn't updated yet). The user's navigation will pick up the
389
+ // new server code when it completes. isNavigating covers the
390
+ // full lifecycle (fetching + streaming, before commit) without
391
+ // blocking on server actions.
392
+ if (eventController.getState().isNavigating) {
393
+ console.log("[Rango] HMR: Skipping — navigation in progress");
394
+ return;
395
+ }
396
+
397
+ console.log("[Rango] HMR: Server update, refetching RSC");
398
+
399
+ const abort = new AbortController();
400
+ hmrAbort = abort;
401
+
402
+ const handle = eventController.startNavigation(window.location.href, {
403
+ replace: true,
285
404
  });
405
+ const streamingToken = handle.startStreaming();
406
+
407
+ const interceptSourceUrl = store.getInterceptSourceUrl();
408
+
409
+ try {
410
+ const { payload, streamComplete } = await client.fetchPartial({
411
+ targetUrl: window.location.href,
412
+ segmentIds: [],
413
+ previousUrl: store.getSegmentState().currentUrl,
414
+ interceptSourceUrl: interceptSourceUrl || undefined,
415
+ routerId: store.getRouterId?.(),
416
+ hmr: true,
417
+ signal: abort.signal,
418
+ });
286
419
 
287
- if (payload.metadata?.isPartial) {
288
- const segments = payload.metadata.segments || [];
289
- const matched = payload.metadata.matched || [];
420
+ if (abort.signal.aborted) return;
290
421
 
291
- // Derive intercept state from the returned payload, not the
292
- // pre-fetch store snapshot. If the HMR edit removed intercept
293
- // behavior, the response won't contain intercept segments.
294
- const responseIsIntercept = segments.some(isInterceptSegment);
422
+ // If the server returned a non-RSC response (404, 500 without
423
+ // error boundary), the payload won't have valid metadata.
424
+ // Reload to recover rather than leaving the page stale.
425
+ if (!payload.metadata) {
426
+ throw new Error("HMR refetch returned invalid payload");
427
+ }
295
428
 
296
- // Sync store intercept state with what the server returned
297
- if (!responseIsIntercept && interceptSourceUrl) {
298
- store.setInterceptSourceUrl(null);
429
+ // Update version BEFORE rebuilding state so that
430
+ // clearHistoryCache() runs first, then the fresh segment
431
+ // cache entry we create below survives.
432
+ //
433
+ // Compare against the bridge's live version, not the init-time
434
+ // `version` const: after the first HMR bump the const is stale, so a
435
+ // later update with an unchanged version would otherwise re-clear the
436
+ // cache and re-broadcast across tabs/apps. The live read fires only
437
+ // on a genuine version change.
438
+ const newVersion = payload.metadata.version;
439
+ const currentVersion = navigationBridge.getVersion();
440
+ if (newVersion && newVersion !== currentVersion) {
441
+ console.log(
442
+ "[Rango] HMR: version changed",
443
+ currentVersion,
444
+ "→",
445
+ newVersion,
446
+ "clearing caches",
447
+ );
448
+ navigationBridge.updateVersion(newVersion);
299
449
  }
300
450
 
301
- store.setSegmentIds(matched);
302
- store.setCurrentUrl(window.location.href);
451
+ // Apply only partial segment updates. A non-partial payload during
452
+ // HMR is transient: the worker route table is still rebuilding after
453
+ // the edit, so the URL momentarily resolves to not-found/catch-all.
454
+ // Skip it -- the debounced follow-up refetch returns the settled
455
+ // route's partial payload and renders it below. We never reload here:
456
+ // a paramless document GET would run the SSR path and surface the
457
+ // not-found page during that same transient.
458
+ if (payload.metadata?.isPartial) {
459
+ const segments = payload.metadata.segments || [];
460
+ const matched = payload.metadata.matched || [];
461
+
462
+ // Derive intercept state from the returned payload, not the
463
+ // pre-fetch store snapshot. If the HMR edit removed intercept
464
+ // behavior, the response won't contain intercept segments.
465
+ const responseIsIntercept = segments.some(isInterceptSegment);
466
+
467
+ // Sync store intercept state with what the server returned
468
+ if (!responseIsIntercept && interceptSourceUrl) {
469
+ store.setInterceptSourceUrl(null);
470
+ }
471
+
472
+ store.setSegmentIds(matched);
473
+ store.setCurrentUrl(window.location.href);
474
+
475
+ const historyKey = generateHistoryKey(window.location.href, {
476
+ intercept: responseIsIntercept,
477
+ });
478
+ store.setHistoryKey(historyKey);
479
+ const currentHandleData = eventController.getHandleState().data;
480
+ store.cacheSegmentsForHistory(
481
+ historyKey,
482
+ segments,
483
+ currentHandleData,
484
+ );
485
+
486
+ const { main, intercept } = splitInterceptSegments(segments);
487
+ store.emitUpdate({
488
+ root: renderSegments(main, {
489
+ interceptSegments: intercept.length > 0 ? intercept : undefined,
490
+ }),
491
+ metadata: payload.metadata,
492
+ });
493
+ }
303
494
 
304
- const historyKey = generateHistoryKey(window.location.href, {
305
- intercept: responseIsIntercept,
306
- });
307
- store.setHistoryKey(historyKey);
308
- const currentHandleData = eventController.getHandleState().data;
309
- store.cacheSegmentsForHistory(
310
- historyKey,
311
- segments,
312
- currentHandleData,
313
- );
314
-
315
- const { main, intercept } = splitInterceptSegments(segments);
316
- store.emitUpdate({
317
- root: renderSegments(main, {
318
- interceptSegments: intercept.length > 0 ? intercept : undefined,
319
- }),
320
- metadata: payload.metadata,
321
- });
495
+ await streamComplete;
496
+ handle.complete(new URL(window.location.href));
497
+ console.log("[Rango] HMR: RSC stream complete");
498
+ } catch (err) {
499
+ if (abort.signal.aborted) return;
500
+ console.warn("[Rango] HMR: Refetch failed, reloading page", err);
501
+ window.location.reload();
502
+ return;
503
+ } finally {
504
+ if (hmrAbort === abort) hmrAbort = null;
505
+ streamingToken.end();
506
+ handle[Symbol.dispose]();
322
507
  }
323
-
324
- await streamComplete;
325
- handle.complete(new URL(window.location.href));
326
- console.log("[RSCRouter] HMR: RSC stream complete");
327
- } finally {
328
- streamingToken.end();
329
- handle[Symbol.dispose]();
330
- }
508
+ }, 200);
331
509
  });
332
510
  }
333
511
 
334
- // Store context for RSCRouter component
512
+ // Store context for Rango component
335
513
  const context: BrowserAppContext = {
336
514
  store,
337
515
  eventController,
@@ -341,20 +519,27 @@ export async function initBrowserApp(
341
519
  themeConfig: effectiveThemeConfig,
342
520
  initialTheme: effectiveInitialTheme,
343
521
  warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
522
+ strictMode: initialPayload.metadata?.strictMode ?? true,
344
523
  version,
524
+ appShellRef,
345
525
  };
346
526
  browserAppContext = context;
347
527
 
528
+ bootLog("initBrowserApp complete -- handing off to hydrateRoot");
348
529
  return context;
349
530
  }
350
531
 
532
+ // Once-flag so the hydration-commit boot log fires a single time (StrictMode
533
+ // re-runs the root effect; the second flush is not a second hydration).
534
+ let hydrationCommitLogged = false;
535
+
351
536
  /**
352
537
  * Get the browser app context. Throws if initBrowserApp hasn't been called.
353
538
  */
354
539
  export function getBrowserAppContext(): BrowserAppContext {
355
540
  if (!browserAppContext) {
356
541
  throw new Error(
357
- "RSCRouter: initBrowserApp() must be called before rendering RSCRouter",
542
+ "Rango: initBrowserApp() must be called before rendering Rango",
358
543
  );
359
544
  }
360
545
  return browserAppContext;
@@ -368,18 +553,18 @@ export function resetBrowserAppContext(): void {
368
553
  }
369
554
 
370
555
  /**
371
- * Props for the RSCRouter component
556
+ * Props for the Rango component
372
557
  */
373
- export interface RSCRouterProps {}
558
+ export interface RangoProps {}
374
559
 
375
560
  /**
376
- * RSCRouter component - renders the RSC router with all internal wiring.
561
+ * Rango component - renders the RSC router with all internal wiring.
377
562
  *
378
563
  * Must be called after initBrowserApp() has completed.
379
564
  *
380
565
  * @example
381
566
  * ```tsx
382
- * import { initBrowserApp, RSCRouter } from "rsc-router/browser";
567
+ * import { initBrowserApp, Rango } from "rsc-router/browser";
383
568
  * import { rscStream } from "rsc-html-stream/client";
384
569
  * import * as rscBrowser from "@vitejs/plugin-rsc/browser";
385
570
  *
@@ -389,14 +574,14 @@ export interface RSCRouterProps {}
389
574
  * hydrateRoot(
390
575
  * document,
391
576
  * <React.StrictMode>
392
- * <RSCRouter />
577
+ * <Rango />
393
578
  * </React.StrictMode>
394
579
  * );
395
580
  * }
396
581
  * main();
397
582
  * ```
398
583
  */
399
- export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
584
+ export function Rango(_props: RangoProps): React.ReactElement {
400
585
  const {
401
586
  store,
402
587
  eventController,
@@ -407,6 +592,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
407
592
  initialTheme,
408
593
  warmupEnabled,
409
594
  version,
595
+ appShellRef,
410
596
  } = getBrowserAppContext();
411
597
 
412
598
  // Signal that the React tree has hydrated. useEffect only fires after
@@ -414,6 +600,10 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
414
600
  // that does not depend on React internals like __reactFiber.
415
601
  React.useEffect(() => {
416
602
  document.documentElement.dataset.hydrated = "";
603
+ if (IS_BROWSER_DEBUG && !hydrationCommitLogged) {
604
+ hydrationCommitLogged = true;
605
+ bootLog("hydration commit (root effect flushed)");
606
+ }
417
607
  }, []);
418
608
 
419
609
  return (
@@ -426,6 +616,8 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
426
616
  initialTheme={initialTheme}
427
617
  warmupEnabled={warmupEnabled}
428
618
  version={version}
619
+ basename={initialPayload.metadata?.basename}
620
+ appShellRef={appShellRef}
429
621
  />
430
622
  );
431
623
  }
@@ -8,7 +8,16 @@
8
8
  * - Supports hash link scrolling
9
9
  */
10
10
 
11
- import { debugLog } from "./logging.js";
11
+ import { debugLog, IS_BROWSER_DEBUG } 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);
12
21
 
13
22
  const SCROLL_STORAGE_KEY = "rsc-router-scroll-positions";
14
23
 
@@ -141,10 +150,12 @@ export function initScrollRestoration(options?: {
141
150
 
142
151
  window.addEventListener("pagehide", handlePageHide);
143
152
 
144
- debugLog(
145
- "[Scroll] Initialized, loaded positions:",
146
- Object.keys(savedScrollPositions).length,
147
- );
153
+ if (IS_BROWSER_DEBUG) {
154
+ debugLog(
155
+ "[Scroll] Initialized, loaded positions:",
156
+ Object.keys(savedScrollPositions).length,
157
+ );
158
+ }
148
159
 
149
160
  return () => {
150
161
  cancelScrollRestorationPolling();
@@ -182,10 +193,15 @@ export function saveCurrentScrollPosition(): void {
182
193
 
183
194
  /**
184
195
  * Persist scroll positions to sessionStorage.
185
- * If the write fails due to quota exceeded, progressively evict the oldest
186
- * entries and retry until it succeeds or the store is empty.
196
+ * If the write fails (typically QuotaExceededError), evict the oldest ~1/4 of
197
+ * entries ONCE and retry the write a single time; if that still fails, remove
198
+ * our storage key entirely so we don't block other sessionStorage consumers.
199
+ * This is a single evict-then-retry-then-clear ladder, not a loop.
200
+ *
201
+ * Exported so that single eviction/retry/clear ladder is unit-testable directly.
202
+ * The browser drives it from the `pagehide` handler.
187
203
  */
188
- function persistToSessionStorage(): void {
204
+ export function persistToSessionStorage(): void {
189
205
  try {
190
206
  sessionStorage.setItem(
191
207
  SCROLL_STORAGE_KEY,
@@ -285,9 +301,13 @@ export function restoreScrollPosition(options?: {
285
301
  return true;
286
302
  }
287
303
 
288
- // Not streaming — scroll immediately
289
- window.scrollTo(0, savedY);
290
- debugLog("[Scroll] Restored position:", savedY, "for key:", key);
304
+ // Not streaming — scroll after React commits and browser paints.
305
+ // startTransition defers the DOM commit, so scrolling synchronously
306
+ // would be overwritten when React replaces the content.
307
+ deferToNextPaint(() => {
308
+ window.scrollTo(0, savedY);
309
+ debugLog("[Scroll] Restored position:", savedY, "for key:", key);
310
+ });
291
311
  return true;
292
312
  }
293
313
 
@@ -319,6 +339,8 @@ export function scrollToHash(): boolean {
319
339
  * Scroll to top of page
320
340
  */
321
341
  export function scrollToTop(): void {
342
+ if (typeof window === "undefined") return;
343
+ if (typeof window.scrollTo !== "function") return;
322
344
  window.scrollTo(0, 0);
323
345
  }
324
346
 
@@ -343,31 +365,43 @@ export function handleNavigationEnd(options: {
343
365
  scroll?: boolean;
344
366
  isStreaming?: () => boolean;
345
367
  }): void {
346
- if (!initialized) {
347
- return;
348
- }
349
-
350
368
  const { restore = false, scroll = true, isStreaming } = options;
351
369
 
352
- // Don't scroll if explicitly disabled
353
- if (scroll === false) {
370
+ // Don't scroll if explicitly disabled or not in a browser
371
+ if (scroll === false || typeof window === "undefined") {
354
372
  return;
355
373
  }
356
374
 
357
- // For back/forward (restore), try to restore saved position
358
- if (restore) {
375
+ // Save/restore requires initialization (sessionStorage, history state).
376
+ // But basic scroll-to-top and hash scrolling work without it — this
377
+ // matters during cross-app navigation where ScrollRestoration unmounts
378
+ // and remounts, creating a brief window where initialized is false.
379
+ if (restore && initialized) {
359
380
  if (restoreScrollPosition({ retryIfStreaming: true, isStreaming })) {
360
381
  return;
361
382
  }
362
383
  // Fall through to hash or top if no saved position
363
384
  }
364
385
 
365
- // Try hash scrolling first
386
+ // scrollToHash / scrollToTop run synchronously here.
387
+ // handleNavigationEnd is invoked from NavigationProvider's
388
+ // useLayoutEffect (post-commit, pre-paint), so a sync scrollTo is
389
+ // captured by the upcoming paint AND by startViewTransition's snapshot.
390
+ // Deferring via rAF here pushed the call past the snapshot capture,
391
+ // making forward navigations wrapped in a layout/route view transition
392
+ // skip scroll-to-top — the live DOM scrolled but the captured snapshot
393
+ // was at the previous scroll position, so the user-facing page stayed
394
+ // visually clamped at the source page's scrollY (often the new tree's
395
+ // max scroll for tall→short navs). Y=0 / a hash element are robust
396
+ // against unmeasured layout, so sync scroll is correct here even
397
+ // before the new tree's scrollHeight settles.
398
+ //
399
+ // (The restore branch above keeps deferToNextPaint because savedY
400
+ // depends on the new tree's max scroll; sync scrollTo against an
401
+ // unmeasured DOM would clamp savedY to whatever the old/zero max was.)
366
402
  if (scrollToHash()) {
367
403
  return;
368
404
  }
369
-
370
- // Default: scroll to top
371
405
  scrollToTop();
372
406
  }
373
407