@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30

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 (297) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +883 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4655 -747
  5. package/package.json +78 -50
  6. package/skills/cache-guide/SKILL.md +262 -0
  7. package/skills/caching/SKILL.md +54 -25
  8. package/skills/composability/SKILL.md +172 -0
  9. package/skills/debug-manifest/SKILL.md +12 -8
  10. package/skills/document-cache/SKILL.md +23 -21
  11. package/skills/fonts/SKILL.md +167 -0
  12. package/skills/hooks/SKILL.md +390 -63
  13. package/skills/host-router/SKILL.md +218 -0
  14. package/skills/intercept/SKILL.md +133 -10
  15. package/skills/layout/SKILL.md +102 -5
  16. package/skills/links/SKILL.md +239 -0
  17. package/skills/loader/SKILL.md +366 -29
  18. package/skills/middleware/SKILL.md +173 -36
  19. package/skills/mime-routes/SKILL.md +128 -0
  20. package/skills/parallel/SKILL.md +80 -3
  21. package/skills/prerender/SKILL.md +643 -0
  22. package/skills/rango/SKILL.md +86 -16
  23. package/skills/response-routes/SKILL.md +411 -0
  24. package/skills/route/SKILL.md +227 -14
  25. package/skills/router-setup/SKILL.md +225 -32
  26. package/skills/tailwind/SKILL.md +129 -0
  27. package/skills/theme/SKILL.md +12 -11
  28. package/skills/typesafety/SKILL.md +401 -75
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +10 -4
  31. package/src/bin/rango.ts +321 -0
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +87 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +20 -4
  38. package/src/browser/logging.ts +55 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +201 -553
  41. package/src/browser/navigation-client.ts +124 -71
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +295 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +267 -317
  46. package/src/browser/prefetch/cache.ts +146 -0
  47. package/src/browser/prefetch/fetch.ts +135 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +42 -0
  50. package/src/browser/prefetch/queue.ts +88 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +173 -73
  53. package/src/browser/react/NavigationProvider.tsx +138 -27
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +37 -0
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +49 -65
  65. package/src/browser/react/use-href.tsx +20 -188
  66. package/src/browser/react/use-link-status.ts +6 -5
  67. package/src/browser/react/use-mount.ts +31 -0
  68. package/src/browser/react/use-navigation.ts +27 -78
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +111 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +83 -0
  79. package/src/browser/server-action-bridge.ts +504 -584
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +92 -57
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +438 -0
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +35 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +10 -15
  114. package/src/client.tsx +114 -135
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +34 -19
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +165 -0
  124. package/src/host/errors.ts +97 -0
  125. package/src/host/index.ts +53 -0
  126. package/src/host/pattern-matcher.ts +214 -0
  127. package/src/host/router.ts +352 -0
  128. package/src/host/testing.ts +79 -0
  129. package/src/host/types.ts +146 -0
  130. package/src/host/utils.ts +25 -0
  131. package/src/href-client.ts +135 -49
  132. package/src/index.rsc.ts +182 -17
  133. package/src/index.ts +238 -24
  134. package/src/internal-debug.ts +11 -0
  135. package/src/loader.rsc.ts +27 -142
  136. package/src/loader.ts +27 -10
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +37 -0
  140. package/src/prerender/store.ts +185 -0
  141. package/src/prerender.ts +463 -0
  142. package/src/reverse.ts +330 -0
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +9 -11
  145. package/src/route-definition/dsl-helpers.ts +934 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1388
  151. package/src/route-map-builder.ts +241 -112
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +70 -9
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +158 -0
  158. package/src/router/handler-context.ts +371 -81
  159. package/src/router/intercept-resolution.ts +395 -0
  160. package/src/router/lazy-includes.ts +234 -0
  161. package/src/router/loader-resolution.ts +215 -122
  162. package/src/router/logging.ts +248 -0
  163. package/src/router/manifest.ts +155 -32
  164. package/src/router/match-api.ts +620 -0
  165. package/src/router/match-context.ts +5 -3
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +80 -93
  168. package/src/router/match-middleware/cache-lookup.ts +382 -9
  169. package/src/router/match-middleware/cache-store.ts +51 -22
  170. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  171. package/src/router/match-middleware/segment-resolution.ts +24 -6
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +34 -29
  174. package/src/router/metrics.ts +235 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +324 -367
  178. package/src/router/pattern-matching.ts +321 -30
  179. package/src/router/prerender-match.ts +400 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +137 -38
  182. package/src/router/router-context.ts +36 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +570 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +198 -0
  189. package/src/router/segment-resolution/revalidation.ts +1241 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -0
  192. package/src/router/segment-wrappers.ts +289 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +239 -0
  197. package/src/router/types.ts +77 -3
  198. package/src/router.ts +688 -3656
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +786 -760
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +5 -25
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +235 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +40 -14
  215. package/src/search-params.ts +230 -0
  216. package/src/segment-system.tsx +57 -61
  217. package/src/server/context.ts +202 -51
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +37 -0
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +422 -70
  223. package/src/server.ts +36 -120
  224. package/src/ssr/index.tsx +157 -26
  225. package/src/static-handler.ts +114 -0
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +687 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +102 -0
  241. package/src/types/segments.ts +148 -0
  242. package/src/types.ts +1 -1577
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -726
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +110 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -782
  261. package/src/vite/plugin-types.ts +131 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  266. package/src/vite/plugins/expose-id-utils.ts +287 -0
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +254 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +510 -0
  280. package/src/vite/router-discovery.ts +785 -0
  281. package/src/vite/utils/ast-handler-extract.ts +517 -0
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -3
  289. package/src/browser/lru-cache.ts +0 -69
  290. package/src/browser/request-controller.ts +0 -164
  291. package/src/cache/memory-store.ts +0 -253
  292. package/src/href-context.ts +0 -33
  293. package/src/href.ts +0 -255
  294. package/src/vite/expose-handle-id.ts +0 -209
  295. package/src/vite/expose-loader-id.ts +0 -357
  296. package/src/vite/expose-location-state-id.ts +0 -177
  297. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -11,8 +11,7 @@ import { createEventController } from "./event-controller.js";
11
11
  import { createNavigationClient } from "./navigation-client.js";
12
12
  import { createServerActionBridge } from "./server-action-bridge.js";
13
13
  import { createNavigationBridge } from "./navigation-bridge.js";
14
- import { NavigationProvider, initHandleDataSync, initSegmentsSync } from "./react/index.js";
15
- import { initThemeConfigSync } from "../theme/theme-context.js";
14
+ import { NavigationProvider } from "./react/index.js";
16
15
  import type {
17
16
  RscPayload,
18
17
  RscBrowserDependencies,
@@ -22,6 +21,12 @@ import type {
22
21
  } from "./types.js";
23
22
  import type { EventController } from "./event-controller.js";
24
23
  import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
24
+ import { initRangoState } from "./rango-state.js";
25
+ import { initPrefetchCache } from "./prefetch/cache.js";
26
+ import {
27
+ isInterceptSegment,
28
+ splitInterceptSegments,
29
+ } from "./intercept-utils.js";
25
30
 
26
31
  // Vite HMR types are provided by vite/client
27
32
 
@@ -104,6 +109,10 @@ export interface BrowserAppContext {
104
109
  themeConfig?: ResolvedThemeConfig | null;
105
110
  /** Initial theme from server */
106
111
  initialTheme?: Theme;
112
+ /** Whether connection warmup is enabled */
113
+ warmupEnabled?: boolean;
114
+ /** App version for prefetch version mismatch detection */
115
+ version?: string;
107
116
  }
108
117
 
109
118
  // Module-level state for the initialized app
@@ -119,9 +128,16 @@ let browserAppContext: BrowserAppContext | null = null;
119
128
  * - Configures HMR support
120
129
  */
121
130
  export async function initBrowserApp(
122
- options: InitBrowserAppOptions
131
+ options: InitBrowserAppOptions,
123
132
  ): Promise<BrowserAppContext> {
124
- const { rscStream, deps, storeOptions, linkInterception = true, themeConfig, initialTheme } = options;
133
+ const {
134
+ rscStream,
135
+ deps,
136
+ storeOptions,
137
+ linkInterception = true,
138
+ themeConfig,
139
+ initialTheme,
140
+ } = options;
125
141
 
126
142
  // Load initial payload from SSR-injected __FLIGHT_DATA__
127
143
  const initialPayload =
@@ -129,8 +145,10 @@ export async function initBrowserApp(
129
145
 
130
146
  // Extract themeConfig and initialTheme from payload if not explicitly provided
131
147
  // This allows virtual entries to work without importing the router
132
- const effectiveThemeConfig = themeConfig ?? initialPayload.metadata?.themeConfig ?? null;
133
- const effectiveInitialTheme = initialTheme ?? initialPayload.metadata?.initialTheme;
148
+ const effectiveThemeConfig =
149
+ themeConfig ?? initialPayload.metadata?.themeConfig ?? null;
150
+ const effectiveInitialTheme =
151
+ initialTheme ?? initialPayload.metadata?.initialTheme;
134
152
 
135
153
  // Get initial segments and compute history key from current URL
136
154
  const initialSegments = (initialPayload.metadata?.segments ??
@@ -151,15 +169,12 @@ export async function initBrowserApp(
151
169
  initialLocation: new URL(window.location.href),
152
170
  });
153
171
 
154
- // Initialize segments state BEFORE hydration to avoid mismatch
155
- initSegmentsSync(initialPayload.metadata?.matched, initialPayload.metadata?.pathname);
156
-
157
- // Initialize theme config for MetaTags (must match SSR state)
158
- initThemeConfigSync(effectiveThemeConfig);
159
-
160
172
  // Initialize event controller with segment order (even without handles)
161
173
  eventController.setHandleData({}, initialPayload.metadata?.matched);
162
174
 
175
+ // Initialize route params
176
+ eventController.setParams(initialPayload.metadata?.params ?? {});
177
+
163
178
  // Initialize handle data from initial payload BEFORE hydration
164
179
  // This ensures useHandle returns correct data during hydration to avoid mismatch
165
180
  // The handles property is an async generator that yields on each push
@@ -169,16 +184,17 @@ export async function initBrowserApp(
169
184
  for await (const handleData of handlesGenerator) {
170
185
  lastHandleData = handleData;
171
186
  }
172
- // Initialize both event controller AND module-level SSR state for hydration compatibility
173
- eventController.setHandleData(lastHandleData, initialPayload.metadata?.matched);
174
- initHandleDataSync(lastHandleData, initialPayload.metadata?.matched);
187
+ // Initialize event controller with initial handle state before hydration.
188
+ eventController.setHandleData(
189
+ lastHandleData,
190
+ initialPayload.metadata?.matched,
191
+ );
175
192
 
176
193
  // Update the initial cache entry with the processed handleData
177
194
  // The cache entry was created by createNavigationStore but without handleData
178
195
  store.updateCacheHandleData(initialHistoryKey, lastHandleData);
179
196
  }
180
197
 
181
-
182
198
  // Create composable utilities
183
199
  const client = createNavigationClient(deps);
184
200
 
@@ -186,12 +202,27 @@ export async function initBrowserApp(
186
202
  const rootLayout = initialPayload.metadata?.rootLayout;
187
203
  const version = initialPayload.metadata?.version;
188
204
 
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");
208
+
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.
211
+ const prefetchCacheTTL = initialPayload.metadata?.prefetchCacheTTL;
212
+ if (prefetchCacheTTL !== undefined) {
213
+ initPrefetchCache(prefetchCacheTTL);
214
+ }
215
+
189
216
  // Create a bound renderSegments that includes rootLayout
190
217
  const renderSegments = (
191
218
  segments: ResolvedSegment[],
192
- options?: RenderSegmentsOptions
219
+ options?: RenderSegmentsOptions,
193
220
  ) => baseRenderSegments(segments, { ...options, rootLayout });
194
221
 
222
+ // Lazy reference for navigation bridge — the action bridge is created first
223
+ // but may need to trigger SPA navigation for action redirects.
224
+ let navigateFn: ((url: string, options?: any) => Promise<void>) | null = null;
225
+
195
226
  // Setup server action bridge
196
227
  const actionBridge = createServerActionBridge({
197
228
  store,
@@ -201,6 +232,13 @@ export async function initBrowserApp(
201
232
  onUpdate: (update) => store.emitUpdate(update),
202
233
  renderSegments,
203
234
  version,
235
+ onNavigate: (url, options) => {
236
+ if (!navigateFn) {
237
+ window.location.href = url;
238
+ return Promise.resolve();
239
+ }
240
+ return navigateFn(url, options);
241
+ },
204
242
  });
205
243
  actionBridge.register();
206
244
 
@@ -214,6 +252,9 @@ export async function initBrowserApp(
214
252
  version,
215
253
  });
216
254
 
255
+ // Connect action redirect → navigation bridge (now that both are initialized)
256
+ navigateFn = (url, options) => navigationBridge.navigate(url, options);
257
+
217
258
  // Optionally enable global link interception
218
259
  if (linkInterception) {
219
260
  navigationBridge.registerLinkInterception();
@@ -232,37 +273,61 @@ export async function initBrowserApp(
232
273
  });
233
274
  const streamingToken = handle.startStreaming();
234
275
 
276
+ const interceptSourceUrl = store.getInterceptSourceUrl();
277
+
235
278
  try {
236
279
  const { payload, streamComplete } = await client.fetchPartial({
237
280
  targetUrl: window.location.href,
238
281
  segmentIds: [],
239
282
  previousUrl: store.getSegmentState().currentUrl,
283
+ interceptSourceUrl: interceptSourceUrl || undefined,
284
+ hmr: true,
240
285
  });
241
286
 
242
287
  if (payload.metadata?.isPartial) {
243
288
  const segments = payload.metadata.segments || [];
244
289
  const matched = payload.metadata.matched || [];
245
290
 
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);
295
+
296
+ // Sync store intercept state with what the server returned
297
+ if (!responseIsIntercept && interceptSourceUrl) {
298
+ store.setInterceptSourceUrl(null);
299
+ }
300
+
246
301
  store.setSegmentIds(matched);
247
302
  store.setCurrentUrl(window.location.href);
248
303
 
249
- const historyKey = generateHistoryKey(window.location.href);
304
+ const historyKey = generateHistoryKey(window.location.href, {
305
+ intercept: responseIsIntercept,
306
+ });
250
307
  store.setHistoryKey(historyKey);
251
308
  const currentHandleData = eventController.getHandleState().data;
252
- store.cacheSegmentsForHistory(historyKey, segments, currentHandleData);
309
+ store.cacheSegmentsForHistory(
310
+ historyKey,
311
+ segments,
312
+ currentHandleData,
313
+ );
253
314
 
315
+ const { main, intercept } = splitInterceptSegments(segments);
254
316
  store.emitUpdate({
255
- root: renderSegments(segments),
317
+ root: renderSegments(main, {
318
+ interceptSegments: intercept.length > 0 ? intercept : undefined,
319
+ }),
256
320
  metadata: payload.metadata,
257
321
  });
258
322
  }
259
323
 
260
324
  await streamComplete;
325
+ handle.complete(new URL(window.location.href));
326
+ console.log("[RSCRouter] HMR: RSC stream complete");
261
327
  } finally {
262
328
  streamingToken.end();
329
+ handle[Symbol.dispose]();
263
330
  }
264
- handle.complete(new URL(window.location.href));
265
- console.log("[RSCRouter] HMR: RSC stream complete");
266
331
  });
267
332
  }
268
333
 
@@ -275,6 +340,8 @@ export async function initBrowserApp(
275
340
  initialTree,
276
341
  themeConfig: effectiveThemeConfig,
277
342
  initialTheme: effectiveInitialTheme,
343
+ warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
344
+ version,
278
345
  };
279
346
  browserAppContext = context;
280
347
 
@@ -287,7 +354,7 @@ export async function initBrowserApp(
287
354
  export function getBrowserAppContext(): BrowserAppContext {
288
355
  if (!browserAppContext) {
289
356
  throw new Error(
290
- "RSCRouter: initBrowserApp() must be called before rendering RSCRouter"
357
+ "RSCRouter: initBrowserApp() must be called before rendering RSCRouter",
291
358
  );
292
359
  }
293
360
  return browserAppContext;
@@ -330,17 +397,35 @@ export interface RSCRouterProps {}
330
397
  * ```
331
398
  */
332
399
  export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
333
- const { store, eventController, bridge, initialPayload, initialTree, themeConfig, initialTheme } =
334
- getBrowserAppContext();
400
+ const {
401
+ store,
402
+ eventController,
403
+ bridge,
404
+ initialPayload,
405
+ initialTree,
406
+ themeConfig,
407
+ initialTheme,
408
+ warmupEnabled,
409
+ version,
410
+ } = getBrowserAppContext();
411
+
412
+ // Signal that the React tree has hydrated. useEffect only fires after
413
+ // hydration completes, so this attribute is a stable readiness marker
414
+ // that does not depend on React internals like __reactFiber.
415
+ React.useEffect(() => {
416
+ document.documentElement.dataset.hydrated = "";
417
+ }, []);
335
418
 
336
419
  return (
337
420
  <NavigationProvider
338
421
  store={store}
339
422
  eventController={eventController}
340
- initialPayload={{ ...initialPayload, root: initialTree }}
423
+ initialPayload={{ root: initialTree, metadata: initialPayload.metadata! }}
341
424
  bridge={bridge}
342
425
  themeConfig={themeConfig}
343
426
  initialTheme={initialTheme}
427
+ warmupEnabled={warmupEnabled}
428
+ version={version}
344
429
  />
345
430
  );
346
431
  }
@@ -8,8 +8,18 @@
8
8
  * - Supports hash link scrolling
9
9
  */
10
10
 
11
+ import { debugLog } from "./logging.js";
12
+
11
13
  const SCROLL_STORAGE_KEY = "rsc-router-scroll-positions";
12
14
 
15
+ /**
16
+ * Maximum number of scroll position entries to retain.
17
+ * When exceeded, the oldest entries (by insertion order) are evicted.
18
+ * 200 entries is well within sessionStorage limits while covering
19
+ * realistic back/forward navigation depth.
20
+ */
21
+ const MAX_SCROLL_ENTRIES = 200;
22
+
13
23
  /**
14
24
  * Interval for polling scroll restoration during streaming (ms).
15
25
  * If content is still loading and we can't scroll to saved position,
@@ -29,6 +39,13 @@ const SCROLL_POLL_TIMEOUT_MS = 5000;
29
39
  */
30
40
  let savedScrollPositions: Record<string, number> = {};
31
41
 
42
+ /**
43
+ * Tracks insertion order of scroll position keys for LRU eviction.
44
+ * Most recent entries are at the end of the array.
45
+ * When a key is updated, it is moved to the end.
46
+ */
47
+ let scrollKeyOrder: string[] = [];
48
+
32
49
  /**
33
50
  * Whether scroll restoration has been initialized
34
51
  */
@@ -37,9 +54,12 @@ let initialized = false;
37
54
  /**
38
55
  * Custom getKey function for determining scroll restoration key
39
56
  */
40
- type GetScrollKeyFunction = (
41
- location: { pathname: string; search: string; hash: string; key: string }
42
- ) => string;
57
+ type GetScrollKeyFunction = (location: {
58
+ pathname: string;
59
+ search: string;
60
+ hash: string;
61
+ key: string;
62
+ }) => string;
43
63
 
44
64
  let customGetKey: GetScrollKeyFunction | null = null;
45
65
 
@@ -99,9 +119,13 @@ export function initScrollRestoration(options?: {
99
119
  const stored = sessionStorage.getItem(SCROLL_STORAGE_KEY);
100
120
  if (stored) {
101
121
  savedScrollPositions = JSON.parse(stored);
122
+ // Rebuild key order from loaded positions.
123
+ // Exact original order is lost across page loads, but this is
124
+ // acceptable -- the important invariant is bounded size.
125
+ scrollKeyOrder = Object.keys(savedScrollPositions);
102
126
  }
103
127
  } catch (e) {
104
- // Ignore parse errors
128
+ // Ignore parse errors, start with empty state
105
129
  }
106
130
 
107
131
  // Ensure current history entry has a key
@@ -117,31 +141,82 @@ export function initScrollRestoration(options?: {
117
141
 
118
142
  window.addEventListener("pagehide", handlePageHide);
119
143
 
120
- console.log("[Scroll] Initialized, loaded positions:", Object.keys(savedScrollPositions).length);
144
+ debugLog(
145
+ "[Scroll] Initialized, loaded positions:",
146
+ Object.keys(savedScrollPositions).length,
147
+ );
121
148
 
122
149
  return () => {
150
+ cancelScrollRestorationPolling();
123
151
  window.removeEventListener("pagehide", handlePageHide);
124
152
  window.history.scrollRestoration = "auto";
125
153
  initialized = false;
154
+ savedScrollPositions = {};
155
+ scrollKeyOrder = [];
126
156
  };
127
157
  }
128
158
 
129
159
  /**
130
- * Save the current scroll position for the current history entry
160
+ * Save the current scroll position for the current history entry.
161
+ * Maintains bounded size by evicting oldest entries when the limit is exceeded.
131
162
  */
132
163
  export function saveCurrentScrollPosition(): void {
133
164
  const key = getScrollKey();
165
+
166
+ // If this key already exists, remove it from its current position
167
+ // in the order array so it can be re-appended at the end (most recent).
168
+ const existingIndex = scrollKeyOrder.indexOf(key);
169
+ if (existingIndex !== -1) {
170
+ scrollKeyOrder.splice(existingIndex, 1);
171
+ }
172
+
134
173
  savedScrollPositions[key] = window.scrollY;
174
+ scrollKeyOrder.push(key);
175
+
176
+ // Evict oldest entries if we exceed the limit
177
+ while (scrollKeyOrder.length > MAX_SCROLL_ENTRIES) {
178
+ const oldestKey = scrollKeyOrder.shift()!;
179
+ delete savedScrollPositions[oldestKey];
180
+ }
135
181
  }
136
182
 
137
183
  /**
138
- * Persist scroll positions to sessionStorage
184
+ * 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.
139
187
  */
140
188
  function persistToSessionStorage(): void {
141
189
  try {
142
- sessionStorage.setItem(SCROLL_STORAGE_KEY, JSON.stringify(savedScrollPositions));
190
+ sessionStorage.setItem(
191
+ SCROLL_STORAGE_KEY,
192
+ JSON.stringify(savedScrollPositions),
193
+ );
143
194
  } catch (e) {
144
- console.warn("[Scroll] Failed to persist to sessionStorage:", e);
195
+ // Likely QuotaExceededError. Evict oldest entries and retry.
196
+ const evictCount = Math.max(1, Math.floor(scrollKeyOrder.length / 4));
197
+ for (let i = 0; i < evictCount && scrollKeyOrder.length > 0; i++) {
198
+ const oldestKey = scrollKeyOrder.shift()!;
199
+ delete savedScrollPositions[oldestKey];
200
+ }
201
+
202
+ try {
203
+ sessionStorage.setItem(
204
+ SCROLL_STORAGE_KEY,
205
+ JSON.stringify(savedScrollPositions),
206
+ );
207
+ } catch (retryErr) {
208
+ // Storage still full after eviction. Clear our key entirely so we
209
+ // don't block other sessionStorage consumers.
210
+ console.warn(
211
+ "[Scroll] Failed to persist to sessionStorage after eviction, clearing scroll data:",
212
+ retryErr,
213
+ );
214
+ try {
215
+ sessionStorage.removeItem(SCROLL_STORAGE_KEY);
216
+ } catch {
217
+ // Nothing more we can do
218
+ }
219
+ }
145
220
  }
146
221
  }
147
222
 
@@ -195,13 +270,13 @@ export function restoreScrollPosition(options?: {
195
270
 
196
271
  if (canScrollToPosition) {
197
272
  window.scrollTo(0, savedY);
198
- console.log("[Scroll] Restored position:", savedY, "for key:", key);
273
+ debugLog("[Scroll] Restored position:", savedY, "for key:", key);
199
274
  return true;
200
275
  }
201
276
 
202
277
  // Scroll as far as we can for now
203
278
  window.scrollTo(0, maxScrollY);
204
- console.log("[Scroll] Partial restore to:", maxScrollY, "target:", savedY);
279
+ debugLog("[Scroll] Partial restore to:", maxScrollY, "target:", savedY);
205
280
 
206
281
  // Poll while streaming until we can scroll to target position
207
282
  if (options?.retryIfStreaming && options?.isStreaming?.()) {
@@ -210,23 +285,24 @@ export function restoreScrollPosition(options?: {
210
285
  pendingPollInterval = setInterval(() => {
211
286
  // Stop if we've exceeded the timeout
212
287
  if (Date.now() - startTime > SCROLL_POLL_TIMEOUT_MS) {
213
- console.log("[Scroll] Polling timeout, giving up");
288
+ debugLog("[Scroll] Polling timeout, giving up");
214
289
  cancelScrollRestorationPolling();
215
290
  return;
216
291
  }
217
292
 
218
293
  // Stop if streaming ended
219
294
  if (!options.isStreaming?.()) {
220
- console.log("[Scroll] Streaming ended, stopping poll");
295
+ debugLog("[Scroll] Streaming ended, stopping poll");
221
296
  cancelScrollRestorationPolling();
222
297
  return;
223
298
  }
224
299
 
225
300
  // Check if we can now scroll to the target position
226
- const currentMaxScrollY = document.documentElement.scrollHeight - window.innerHeight;
301
+ const currentMaxScrollY =
302
+ document.documentElement.scrollHeight - window.innerHeight;
227
303
  if (savedY <= currentMaxScrollY) {
228
304
  window.scrollTo(0, savedY);
229
- console.log("[Scroll] Poll restored position:", savedY);
305
+ debugLog("[Scroll] Poll restored position:", savedY);
230
306
  cancelScrollRestorationPolling();
231
307
  }
232
308
  }, SCROLL_POLL_INTERVAL_MS);
@@ -249,7 +325,7 @@ export function scrollToHash(): boolean {
249
325
  const element = document.getElementById(id);
250
326
  if (element) {
251
327
  element.scrollIntoView();
252
- console.log("[Scroll] Scrolled to hash element:", id);
328
+ debugLog("[Scroll] Scrolled to hash element:", id);
253
329
  return true;
254
330
  }
255
331
  } catch (e) {