@rangojs/router 0.0.0-experimental.5 → 0.0.0-experimental.51

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 (302) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1606 -0
  4. package/dist/vite/index.js +4567 -769
  5. package/package.json +77 -58
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +89 -30
  18. package/skills/loader/SKILL.md +403 -43
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +204 -1
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +257 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +328 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +92 -64
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/intercept-utils.ts +52 -0
  38. package/src/browser/link-interceptor.ts +24 -4
  39. package/src/browser/logging.ts +55 -0
  40. package/src/browser/merge-segment-loaders.ts +20 -12
  41. package/src/browser/navigation-bridge.ts +282 -557
  42. package/src/browser/navigation-client.ts +157 -71
  43. package/src/browser/navigation-store.ts +33 -50
  44. package/src/browser/navigation-transaction.ts +297 -0
  45. package/src/browser/network-error-handler.ts +61 -0
  46. package/src/browser/partial-update.ts +303 -310
  47. package/src/browser/prefetch/cache.ts +206 -0
  48. package/src/browser/prefetch/fetch.ts +144 -0
  49. package/src/browser/prefetch/observer.ts +65 -0
  50. package/src/browser/prefetch/policy.ts +48 -0
  51. package/src/browser/prefetch/queue.ts +144 -0
  52. package/src/browser/prefetch/resource-ready.ts +77 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +193 -73
  55. package/src/browser/react/NavigationProvider.tsx +160 -13
  56. package/src/browser/react/context.ts +6 -0
  57. package/src/browser/react/filter-segment-order.ts +11 -0
  58. package/src/browser/react/index.ts +12 -12
  59. package/src/browser/react/location-state-shared.ts +95 -53
  60. package/src/browser/react/location-state.ts +60 -15
  61. package/src/browser/react/mount-context.ts +24 -1
  62. package/src/browser/react/nonce-context.ts +23 -0
  63. package/src/browser/react/shallow-equal.ts +27 -0
  64. package/src/browser/react/use-action.ts +29 -51
  65. package/src/browser/react/use-client-cache.ts +5 -3
  66. package/src/browser/react/use-handle.ts +32 -79
  67. package/src/browser/react/use-href.tsx +2 -2
  68. package/src/browser/react/use-link-status.ts +6 -5
  69. package/src/browser/react/use-navigation.ts +22 -63
  70. package/src/browser/react/use-params.ts +65 -0
  71. package/src/browser/react/use-pathname.ts +47 -0
  72. package/src/browser/react/use-router.ts +63 -0
  73. package/src/browser/react/use-search-params.ts +56 -0
  74. package/src/browser/react/use-segments.ts +80 -97
  75. package/src/browser/response-adapter.ts +73 -0
  76. package/src/browser/rsc-router.tsx +188 -55
  77. package/src/browser/scroll-restoration.ts +117 -44
  78. package/src/browser/segment-reconciler.ts +221 -0
  79. package/src/browser/segment-structure-assert.ts +16 -0
  80. package/src/browser/server-action-bridge.ts +504 -599
  81. package/src/browser/shallow.ts +6 -1
  82. package/src/browser/types.ts +118 -47
  83. package/src/browser/validate-redirect-origin.ts +29 -0
  84. package/src/build/generate-manifest.ts +235 -24
  85. package/src/build/generate-route-types.ts +36 -0
  86. package/src/build/index.ts +13 -0
  87. package/src/build/route-trie.ts +265 -0
  88. package/src/build/route-types/ast-helpers.ts +25 -0
  89. package/src/build/route-types/ast-route-extraction.ts +98 -0
  90. package/src/build/route-types/codegen.ts +102 -0
  91. package/src/build/route-types/include-resolution.ts +411 -0
  92. package/src/build/route-types/param-extraction.ts +48 -0
  93. package/src/build/route-types/per-module-writer.ts +128 -0
  94. package/src/build/route-types/router-processing.ts +479 -0
  95. package/src/build/route-types/scan-filter.ts +78 -0
  96. package/src/build/runtime-discovery.ts +231 -0
  97. package/src/cache/background-task.ts +34 -0
  98. package/src/cache/cache-key-utils.ts +44 -0
  99. package/src/cache/cache-policy.ts +125 -0
  100. package/src/cache/cache-runtime.ts +342 -0
  101. package/src/cache/cache-scope.ts +167 -309
  102. package/src/cache/cf/cf-cache-store.ts +571 -17
  103. package/src/cache/cf/index.ts +13 -3
  104. package/src/cache/document-cache.ts +116 -77
  105. package/src/cache/handle-capture.ts +81 -0
  106. package/src/cache/handle-snapshot.ts +41 -0
  107. package/src/cache/index.ts +1 -15
  108. package/src/cache/memory-segment-store.ts +191 -13
  109. package/src/cache/profile-registry.ts +73 -0
  110. package/src/cache/read-through-swr.ts +134 -0
  111. package/src/cache/segment-codec.ts +256 -0
  112. package/src/cache/taint.ts +153 -0
  113. package/src/cache/types.ts +72 -122
  114. package/src/client.rsc.tsx +3 -1
  115. package/src/client.tsx +106 -126
  116. package/src/component-utils.ts +4 -4
  117. package/src/components/DefaultDocument.tsx +5 -1
  118. package/src/context-var.ts +86 -0
  119. package/src/debug.ts +19 -9
  120. package/src/errors.ts +108 -2
  121. package/src/handle.ts +15 -29
  122. package/src/handles/MetaTags.tsx +73 -20
  123. package/src/handles/breadcrumbs.ts +66 -0
  124. package/src/handles/index.ts +1 -0
  125. package/src/handles/meta.ts +30 -13
  126. package/src/host/cookie-handler.ts +165 -0
  127. package/src/host/errors.ts +97 -0
  128. package/src/host/index.ts +53 -0
  129. package/src/host/pattern-matcher.ts +214 -0
  130. package/src/host/router.ts +352 -0
  131. package/src/host/testing.ts +79 -0
  132. package/src/host/types.ts +146 -0
  133. package/src/host/utils.ts +25 -0
  134. package/src/href-client.ts +119 -29
  135. package/src/index.rsc.ts +153 -19
  136. package/src/index.ts +211 -30
  137. package/src/internal-debug.ts +11 -0
  138. package/src/loader.rsc.ts +26 -147
  139. package/src/loader.ts +27 -10
  140. package/src/network-error-thrower.tsx +3 -1
  141. package/src/outlet-provider.tsx +45 -0
  142. package/src/prerender/param-hash.ts +37 -0
  143. package/src/prerender/store.ts +185 -0
  144. package/src/prerender.ts +463 -0
  145. package/src/reverse.ts +330 -0
  146. package/src/root-error-boundary.tsx +41 -29
  147. package/src/route-content-wrapper.tsx +7 -4
  148. package/src/route-definition/dsl-helpers.ts +959 -0
  149. package/src/route-definition/helper-factories.ts +200 -0
  150. package/src/route-definition/helpers-types.ts +431 -0
  151. package/src/route-definition/index.ts +52 -0
  152. package/src/route-definition/redirect.ts +93 -0
  153. package/src/route-definition.ts +1 -1428
  154. package/src/route-map-builder.ts +217 -123
  155. package/src/route-name.ts +53 -0
  156. package/src/route-types.ts +59 -8
  157. package/src/router/content-negotiation.ts +116 -0
  158. package/src/router/debug-manifest.ts +72 -0
  159. package/src/router/error-handling.ts +9 -9
  160. package/src/router/find-match.ts +160 -0
  161. package/src/router/handler-context.ts +400 -84
  162. package/src/router/intercept-resolution.ts +397 -0
  163. package/src/router/lazy-includes.ts +237 -0
  164. package/src/router/loader-resolution.ts +222 -123
  165. package/src/router/logging.ts +251 -0
  166. package/src/router/manifest.ts +154 -35
  167. package/src/router/match-api.ts +620 -0
  168. package/src/router/match-context.ts +5 -3
  169. package/src/router/match-handlers.ts +440 -0
  170. package/src/router/match-middleware/background-revalidation.ts +108 -93
  171. package/src/router/match-middleware/cache-lookup.ts +440 -10
  172. package/src/router/match-middleware/cache-store.ts +98 -26
  173. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  174. package/src/router/match-middleware/segment-resolution.ts +27 -6
  175. package/src/router/match-pipelines.ts +10 -45
  176. package/src/router/match-result.ts +55 -33
  177. package/src/router/metrics.ts +240 -15
  178. package/src/router/middleware-cookies.ts +55 -0
  179. package/src/router/middleware-types.ts +226 -0
  180. package/src/router/middleware.ts +327 -369
  181. package/src/router/pattern-matching.ts +211 -43
  182. package/src/router/prerender-match.ts +402 -0
  183. package/src/router/preview-match.ts +170 -0
  184. package/src/router/revalidation.ts +137 -38
  185. package/src/router/router-context.ts +41 -21
  186. package/src/router/router-interfaces.ts +452 -0
  187. package/src/router/router-options.ts +592 -0
  188. package/src/router/router-registry.ts +24 -0
  189. package/src/router/segment-resolution/fresh.ts +683 -0
  190. package/src/router/segment-resolution/helpers.ts +263 -0
  191. package/src/router/segment-resolution/loader-cache.ts +199 -0
  192. package/src/router/segment-resolution/revalidation.ts +1301 -0
  193. package/src/router/segment-resolution/static-store.ts +67 -0
  194. package/src/router/segment-resolution.ts +21 -0
  195. package/src/router/segment-wrappers.ts +291 -0
  196. package/src/router/telemetry-otel.ts +299 -0
  197. package/src/router/telemetry.ts +300 -0
  198. package/src/router/timeout.ts +148 -0
  199. package/src/router/trie-matching.ts +239 -0
  200. package/src/router/types.ts +77 -3
  201. package/src/router.ts +665 -4182
  202. package/src/rsc/handler-context.ts +45 -0
  203. package/src/rsc/handler.ts +764 -754
  204. package/src/rsc/helpers.ts +140 -6
  205. package/src/rsc/index.ts +0 -20
  206. package/src/rsc/loader-fetch.ts +209 -0
  207. package/src/rsc/manifest-init.ts +86 -0
  208. package/src/rsc/nonce.ts +14 -0
  209. package/src/rsc/origin-guard.ts +141 -0
  210. package/src/rsc/progressive-enhancement.ts +379 -0
  211. package/src/rsc/response-error.ts +37 -0
  212. package/src/rsc/response-route-handler.ts +347 -0
  213. package/src/rsc/rsc-rendering.ts +237 -0
  214. package/src/rsc/runtime-warnings.ts +42 -0
  215. package/src/rsc/server-action.ts +348 -0
  216. package/src/rsc/ssr-setup.ts +128 -0
  217. package/src/rsc/types.ts +38 -11
  218. package/src/search-params.ts +230 -0
  219. package/src/segment-system.tsx +172 -21
  220. package/src/server/context.ts +278 -58
  221. package/src/server/cookie-store.ts +190 -0
  222. package/src/server/fetchable-loader-store.ts +37 -0
  223. package/src/server/handle-store.ts +94 -15
  224. package/src/server/loader-registry.ts +15 -56
  225. package/src/server/request-context.ts +474 -74
  226. package/src/server.ts +35 -128
  227. package/src/ssr/index.tsx +101 -31
  228. package/src/static-handler.ts +114 -0
  229. package/src/theme/ThemeProvider.tsx +21 -15
  230. package/src/theme/ThemeScript.tsx +5 -5
  231. package/src/theme/constants.ts +5 -2
  232. package/src/theme/index.ts +4 -14
  233. package/src/theme/theme-context.ts +4 -30
  234. package/src/theme/theme-script.ts +21 -18
  235. package/src/types/boundaries.ts +158 -0
  236. package/src/types/cache-types.ts +198 -0
  237. package/src/types/error-types.ts +192 -0
  238. package/src/types/global-namespace.ts +100 -0
  239. package/src/types/handler-context.ts +777 -0
  240. package/src/types/index.ts +88 -0
  241. package/src/types/loader-types.ts +183 -0
  242. package/src/types/route-config.ts +170 -0
  243. package/src/types/route-entry.ts +109 -0
  244. package/src/types/segments.ts +150 -0
  245. package/src/types.ts +1 -1623
  246. package/src/urls/include-helper.ts +197 -0
  247. package/src/urls/index.ts +53 -0
  248. package/src/urls/path-helper-types.ts +339 -0
  249. package/src/urls/path-helper.ts +329 -0
  250. package/src/urls/pattern-types.ts +95 -0
  251. package/src/urls/response-types.ts +106 -0
  252. package/src/urls/type-extraction.ts +372 -0
  253. package/src/urls/urls-function.ts +98 -0
  254. package/src/urls.ts +1 -802
  255. package/src/use-loader.tsx +85 -77
  256. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  257. package/src/vite/discovery/discover-routers.ts +344 -0
  258. package/src/vite/discovery/prerender-collection.ts +385 -0
  259. package/src/vite/discovery/route-types-writer.ts +258 -0
  260. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  261. package/src/vite/discovery/state.ts +108 -0
  262. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  263. package/src/vite/index.ts +11 -782
  264. package/src/vite/plugin-types.ts +48 -0
  265. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  266. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  267. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  268. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  269. package/src/vite/plugins/expose-id-utils.ts +287 -0
  270. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  271. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  272. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  273. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  274. package/src/vite/plugins/expose-ids/types.ts +45 -0
  275. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  276. package/src/vite/plugins/refresh-cmd.ts +65 -0
  277. package/src/vite/plugins/use-cache-transform.ts +323 -0
  278. package/src/vite/plugins/version-injector.ts +83 -0
  279. package/src/vite/plugins/version-plugin.ts +266 -0
  280. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +27 -16
  281. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  282. package/src/vite/rango.ts +445 -0
  283. package/src/vite/router-discovery.ts +777 -0
  284. package/src/vite/utils/ast-handler-extract.ts +517 -0
  285. package/src/vite/utils/banner.ts +36 -0
  286. package/src/vite/utils/bundle-analysis.ts +137 -0
  287. package/src/vite/utils/manifest-utils.ts +70 -0
  288. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  289. package/src/vite/utils/prerender-utils.ts +189 -0
  290. package/src/vite/utils/shared-utils.ts +169 -0
  291. package/CLAUDE.md +0 -43
  292. package/src/browser/lru-cache.ts +0 -69
  293. package/src/browser/request-controller.ts +0 -164
  294. package/src/cache/memory-store.ts +0 -253
  295. package/src/href-context.ts +0 -33
  296. package/src/href.ts +0 -255
  297. package/src/server/route-manifest-cache.ts +0 -173
  298. package/src/vite/expose-handle-id.ts +0 -209
  299. package/src/vite/expose-loader-id.ts +0 -426
  300. package/src/vite/expose-location-state-id.ts +0 -177
  301. package/src/warmup/connection-warmup.tsx +0 -94
  302. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -3,8 +3,10 @@
3
3
  import React, {
4
4
  useState,
5
5
  useEffect,
6
+ useLayoutEffect,
6
7
  useCallback,
7
8
  useMemo,
9
+ useRef,
8
10
  use,
9
11
  type ReactNode,
10
12
  } from "react";
@@ -14,7 +16,7 @@ import {
14
16
  } from "./context.js";
15
17
  import type {
16
18
  NavigationStore,
17
- RscPayload,
19
+ NavigationUpdate,
18
20
  NavigateOptions,
19
21
  NavigationBridge,
20
22
  } from "../types.js";
@@ -22,8 +24,10 @@ import type { EventController } from "../event-controller.js";
22
24
  import { RootErrorBoundary } from "../../root-error-boundary.js";
23
25
  import type { HandleData } from "../types.js";
24
26
  import { ThemeProvider } from "../../theme/ThemeProvider.js";
27
+ import { NonceContext } from "./nonce-context.js";
25
28
  import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
26
- import { ConnectionWarmup } from "../../warmup/connection-warmup.js";
29
+ import { cancelAllPrefetches } from "../prefetch/queue.js";
30
+ import { handleNavigationEnd } from "../scroll-restoration.js";
27
31
 
28
32
  /**
29
33
  * Process handles from an async generator, updating the event controller
@@ -43,7 +47,7 @@ async function processHandles(
43
47
  matched?: string[];
44
48
  isPartial?: boolean;
45
49
  historyKey: string;
46
- }
50
+ },
47
51
  ): Promise<void> {
48
52
  const { eventController, store, matched, isPartial, historyKey } = opts;
49
53
 
@@ -54,7 +58,7 @@ async function processHandles(
54
58
  // the current route's breadcrumbs (e.g., quick popstate after clicking a link).
55
59
  if (historyKey !== store.getHistoryKey()) {
56
60
  console.log(
57
- "[NavigationProvider] Stopping handle processing - user navigated away"
61
+ "[NavigationProvider] Stopping handle processing - user navigated away",
58
62
  );
59
63
  return;
60
64
  }
@@ -101,9 +105,9 @@ export interface NavigationProviderProps {
101
105
  eventController: EventController;
102
106
 
103
107
  /**
104
- * Initial RSC payload from server
108
+ * Initial rendered tree + metadata from server payload
105
109
  */
106
- initialPayload: RscPayload;
110
+ initialPayload: NavigationUpdate;
107
111
 
108
112
  /**
109
113
  * Navigation bridge for handling navigation
@@ -124,9 +128,15 @@ export interface NavigationProviderProps {
124
128
 
125
129
  /**
126
130
  * Whether connection warmup is enabled.
127
- * When true, renders ConnectionWarmup to keep TLS alive after idle periods.
131
+ * When true, keeps TLS alive by sending HEAD requests after idle periods.
128
132
  */
129
133
  warmupEnabled?: boolean;
134
+
135
+ /**
136
+ * App version from server payload (stable, immutable).
137
+ * Forwarded to prefetch requests for version mismatch detection.
138
+ */
139
+ version?: string;
130
140
  }
131
141
 
132
142
  /**
@@ -158,6 +168,7 @@ export function NavigationProvider({
158
168
  themeConfig,
159
169
  initialTheme,
160
170
  warmupEnabled,
171
+ version,
161
172
  }: NavigationProviderProps): ReactNode {
162
173
  // Track current payload for rendering (this triggers re-renders)
163
174
  const [payload, setPayload] = useState(initialPayload);
@@ -169,7 +180,7 @@ export function NavigationProvider({
169
180
  async (url: string, options?: NavigateOptions): Promise<void> => {
170
181
  await bridge.navigate(url, options);
171
182
  },
172
- []
183
+ [],
173
184
  );
174
185
 
175
186
  /**
@@ -186,18 +197,148 @@ export function NavigationProvider({
186
197
  eventController,
187
198
  navigate,
188
199
  refresh,
200
+ version,
189
201
  }),
190
- []
202
+ [],
191
203
  );
192
204
 
205
+ // Connection warmup: keep TLS alive after idle periods.
206
+ // After 60s of no user interaction, marks connection as "cold".
207
+ // On next interaction or visibility change, sends a HEAD request to warm TLS
208
+ // before the user actually clicks a link.
209
+ useEffect(() => {
210
+ if (!warmupEnabled) return;
211
+
212
+ const IDLE_TIMEOUT = 60_000;
213
+ const DEBOUNCE_DELAY = 150;
214
+
215
+ let idleTimer: ReturnType<typeof setTimeout> | undefined;
216
+ let debounceTimer: ReturnType<typeof setTimeout> | undefined;
217
+ let isCold = false;
218
+ let warmupListenersAttached = false;
219
+
220
+ function sendWarmup() {
221
+ isCold = false;
222
+ fetch("/?_rsc_warmup", { method: "HEAD" }).catch(() => {});
223
+ }
224
+
225
+ function triggerWarmup() {
226
+ if (!isCold) return;
227
+ clearTimeout(debounceTimer);
228
+ debounceTimer = setTimeout(() => {
229
+ sendWarmup();
230
+ detachWarmupListeners();
231
+ resetIdleTimer();
232
+ }, DEBOUNCE_DELAY);
233
+ }
234
+
235
+ function onVisibilityChange() {
236
+ if (document.visibilityState === "visible" && isCold) {
237
+ triggerWarmup();
238
+ }
239
+ }
240
+
241
+ function attachWarmupListeners() {
242
+ if (warmupListenersAttached) return;
243
+ warmupListenersAttached = true;
244
+ document.addEventListener("visibilitychange", onVisibilityChange);
245
+ document.addEventListener("mousemove", triggerWarmup, { once: true });
246
+ document.addEventListener("touchstart", triggerWarmup, { once: true });
247
+ }
248
+
249
+ function detachWarmupListeners() {
250
+ warmupListenersAttached = false;
251
+ document.removeEventListener("visibilitychange", onVisibilityChange);
252
+ document.removeEventListener("mousemove", triggerWarmup);
253
+ document.removeEventListener("touchstart", triggerWarmup);
254
+ }
255
+
256
+ function markCold() {
257
+ isCold = true;
258
+ attachWarmupListeners();
259
+ }
260
+
261
+ function resetIdleTimer() {
262
+ clearTimeout(idleTimer);
263
+ isCold = false;
264
+ idleTimer = setTimeout(markCold, IDLE_TIMEOUT);
265
+ }
266
+
267
+ // Activity events that reset the idle timer
268
+ const activityEvents = [
269
+ "mousemove",
270
+ "keydown",
271
+ "touchstart",
272
+ "scroll",
273
+ ] as const;
274
+ const activityOptions: AddEventListenerOptions = { passive: true };
275
+
276
+ for (const event of activityEvents) {
277
+ document.addEventListener(event, resetIdleTimer, activityOptions);
278
+ }
279
+
280
+ resetIdleTimer();
281
+
282
+ return () => {
283
+ clearTimeout(idleTimer);
284
+ clearTimeout(debounceTimer);
285
+ detachWarmupListeners();
286
+ for (const event of activityEvents) {
287
+ document.removeEventListener(event, resetIdleTimer);
288
+ }
289
+ };
290
+ }, [warmupEnabled]);
291
+
292
+ // Cancel speculative prefetches when navigation starts.
293
+ // Viewport/render prefetches should not compete with navigation fetches.
294
+ useEffect(() => {
295
+ let wasIdle = true;
296
+ const unsub = eventController.subscribe(() => {
297
+ const state = eventController.getState();
298
+ const isIdle = state.state === "idle" && !state.isStreaming;
299
+ if (wasIdle && !isIdle) {
300
+ cancelAllPrefetches();
301
+ }
302
+ wasIdle = isIdle;
303
+ });
304
+ return unsub;
305
+ }, [eventController]);
306
+
307
+ // Pending scroll action to apply after React commits
308
+ const pendingScrollRef = useRef<NavigationUpdate["scroll"]>(undefined);
309
+
310
+ // Apply scroll after React commits the new content to the DOM
311
+ useLayoutEffect(() => {
312
+ const scrollAction = pendingScrollRef.current;
313
+ if (!scrollAction) return;
314
+ pendingScrollRef.current = undefined;
315
+
316
+ if (scrollAction.enabled === false) return;
317
+
318
+ handleNavigationEnd({
319
+ restore: scrollAction.restore,
320
+ scroll: scrollAction.enabled,
321
+ isStreaming: scrollAction.isStreaming,
322
+ });
323
+ });
324
+
193
325
  // Subscribe to UI updates (for re-rendering the tree)
194
326
  useEffect(() => {
195
327
  const unsubscribe = store.onUpdate((update) => {
328
+ // Capture scroll intent — it will be applied in useLayoutEffect
329
+ // after React commits this state update to the DOM.
330
+ // Always assign (even undefined) to clear stale scroll from prior navigations,
331
+ // so server actions or error updates don't accidentally replay old scroll.
332
+ pendingScrollRef.current = update.scroll;
333
+
196
334
  setPayload({
197
335
  root: update.root,
198
336
  metadata: update.metadata,
199
337
  });
200
338
 
339
+ // Update route params
340
+ eventController.setParams(update.metadata.params ?? {});
341
+
201
342
  // Update handle data progressively as it streams in
202
343
  if (update.metadata.handles) {
203
344
  // Capture historyKey now - by the time async processing completes,
@@ -211,7 +352,7 @@ export function NavigationProvider({
211
352
  isPartial: update.metadata.isPartial,
212
353
  historyKey,
213
354
  }).catch((err) =>
214
- console.error("[NavigationProvider] Error consuming handles:", err)
355
+ console.error("[NavigationProvider] Error consuming handles:", err),
215
356
  );
216
357
  } else if (update.metadata.cachedHandleData) {
217
358
  // For back/forward navigation from cache, restore the cached handleData
@@ -219,14 +360,14 @@ export function NavigationProvider({
219
360
  eventController.setHandleData(
220
361
  update.metadata.cachedHandleData,
221
362
  update.metadata.matched,
222
- false // full replace - restore entire cached state
363
+ false, // full replace - restore entire cached state
223
364
  );
224
365
  } else if (update.metadata.matched) {
225
366
  // For cached navigations without handleData, update segmentOrder to clean up stale data
226
367
  eventController.setHandleData(
227
368
  {}, // Empty data - all existing data not in matched will be cleaned up
228
369
  update.metadata.matched,
229
- true // partial update - will clean up segments not in matched
370
+ true, // partial update - will clean up segments not in matched
230
371
  );
231
372
  }
232
373
  });
@@ -257,10 +398,16 @@ export function NavigationProvider({
257
398
  );
258
399
  }
259
400
 
401
+ // Match SSR tree shape: NonceContext.Provider is always present so
402
+ // hydration sees the same component tree. Value is undefined on the
403
+ // client — CSP nonces are a server-side HTML concern.
404
+ content = (
405
+ <NonceContext.Provider value={undefined}>{content}</NonceContext.Provider>
406
+ );
407
+
260
408
  return (
261
409
  <NavigationStoreContext.Provider value={contextValue}>
262
410
  {content}
263
- {warmupEnabled && <ConnectionWarmup />}
264
411
  </NavigationStoreContext.Provider>
265
412
  );
266
413
  }
@@ -41,6 +41,12 @@ export interface NavigationStoreContextValue {
41
41
  * @returns Promise that resolves when refresh is complete
42
42
  */
43
43
  refresh: () => Promise<void>;
44
+
45
+ /**
46
+ * App version from server payload (stable, immutable).
47
+ * Used in prefetch requests for version mismatch detection.
48
+ */
49
+ version: string | undefined;
44
50
  }
45
51
 
46
52
  /**
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Filter segment IDs to only include routes and layouts.
3
+ * Excludes parallels (contain .@) and loaders (contain D followed by digit).
4
+ */
5
+ export function filterSegmentOrder(matched: string[]): string[] {
6
+ return matched.filter((id) => {
7
+ if (id.includes(".@")) return false;
8
+ if (/D\d+\./.test(id)) return false;
9
+ return true;
10
+ });
11
+ }
@@ -1,20 +1,24 @@
1
1
  // React exports for browser navigation
2
2
 
3
3
  // Hook with Zustand-style selectors
4
- export {
5
- useNavigation,
6
- type NavigationMethods,
7
- type NavigationValue,
8
- } from "./use-navigation.js";
4
+ export { useNavigation } from "./use-navigation.js";
5
+
6
+ // Router actions hook (stable reference, no re-renders)
7
+ export { useRouter } from "./use-router.js";
8
+
9
+ // URL hooks
10
+ export { usePathname } from "./use-pathname.js";
11
+ export { useSearchParams } from "./use-search-params.js";
12
+ export { useParams } from "./use-params.js";
9
13
 
10
14
  // Action state tracking hook
11
15
  export { useAction, type TrackedActionState } from "./use-action.js";
12
16
 
13
17
  // Segments state hook
14
- export { useSegments, initSegmentsSync, type SegmentsState } from "./use-segments.js";
18
+ export { useSegments, type SegmentsState } from "./use-segments.js";
15
19
 
16
20
  // Handle data hook
17
- export { useHandle, initHandleDataSync } from "./use-handle.js";
21
+ export { useHandle } from "./use-handle.js";
18
22
 
19
23
  // Client cache controls hook
20
24
  export {
@@ -35,11 +39,7 @@ export {
35
39
  } from "./context.js";
36
40
 
37
41
  // Link component
38
- export {
39
- Link,
40
- type LinkProps,
41
- type PrefetchStrategy,
42
- } from "./Link.js";
42
+ export { Link, type LinkProps, type PrefetchStrategy } from "./Link.js";
43
43
 
44
44
  // Link status hook
45
45
  export { useLinkStatus, type LinkStatus } from "./use-link-status.js";
@@ -4,11 +4,22 @@
4
4
  */
5
5
 
6
6
  /**
7
- * Internal entry representing a state value with its unique key
7
+ * Internal entry representing a state value with its unique key.
8
+ * When __rsc_ls_lazy is true, __rsc_ls_value holds a getter function
9
+ * that is called at navigation time (not at entry creation time).
8
10
  */
9
11
  export interface LocationStateEntry {
10
12
  readonly __rsc_ls_key: string;
11
13
  readonly __rsc_ls_value: unknown;
14
+ readonly __rsc_ls_lazy?: boolean;
15
+ }
16
+
17
+ /**
18
+ * Options for createLocationState
19
+ */
20
+ export interface LocationStateOptions {
21
+ /** When true, the state is cleared from history after first read (flash message pattern) */
22
+ flash?: boolean;
12
23
  }
13
24
 
14
25
  /**
@@ -19,84 +30,113 @@ export interface LocationStateEntry {
19
30
  */
20
31
  export interface LocationStateDefinition<TArgs extends unknown[], TState> {
21
32
  (...args: TArgs): LocationStateEntry;
22
- readonly __rsc_ls_key: string;
33
+ /** Injected by Vite plugin - do not set manually */
34
+ __rsc_ls_key: string;
35
+ /** Whether this state auto-clears after first read */
36
+ readonly __rsc_ls_flash: boolean;
37
+ /** Read the current value from history.state (client-side only, undefined during SSR) */
38
+ read(): TState | undefined;
23
39
  }
24
40
 
25
- // Track used keys to detect duplicates in development
26
- const usedKeys = new Set<string>();
27
-
28
41
  /**
29
42
  * Create a type-safe location state definition
30
43
  *
31
- * The key is auto-generated by the Vite exposeLocationStateId plugin based on
32
- * file path and export name. No manual key required.
44
+ * The key is auto-injected by the Vite exposeInternalIds plugin as a property
45
+ * based on file path and export name. No manual key required.
33
46
  *
34
- * @param key Auto-injected by Vite plugin, do not provide manually
47
+ * @param options Optional configuration
35
48
  * @returns A typed state definition for use with Link and useLocationState
36
49
  *
37
50
  * @example
38
51
  * ```typescript
39
- * // Define typed state (key auto-generated from file + export)
52
+ * // Persistent state (survives back/forward)
40
53
  * export const ProductState = createLocationState<{ name: string; price: number }>();
41
54
  *
42
- * // Use in Link - state is captured at click time
43
- * <Link to="/product/123" state={[ProductState({ name: product.name, price: product.price })]}>
44
- * View Product
45
- * </Link>
55
+ * // Flash state (cleared after first read)
56
+ * export const FlashMessage = createLocationState<{ text: string }>({ flash: true });
46
57
  *
47
- * // Multiple states
48
- * <Link to="/checkout" state={[ProductState(productData), CartState(cartData)]}>
49
- * Checkout
50
- * </Link>
58
+ * // Use in Link
59
+ * <Link to="/product/123" state={[ProductState({ name: "Widget", price: 9.99 })]}>
51
60
  *
52
- * // For lazy evaluation (click-time), pass a getter
53
- * <Link to="/product" state={[ProductState(() => ({ name: product.name }))]}>
61
+ * // Just-in-time typed state (getter called at click time, not render time).
62
+ * // Must be in a client component — the getter function can't cross the RSC boundary.
63
+ * <Link
64
+ * to="/product/123"
65
+ * state={[ProductState(() => ({ name: product.name, price: product.price }))]}
66
+ * >
54
67
  *
55
- * // Read with type safety
56
- * const productState = useLocationState(ProductState);
57
- * // productState: { name: string; price: number } | undefined
68
+ * // Read with hook (reactive)
69
+ * const product = useLocationState(ProductState);
70
+ *
71
+ * // Read without hook (snapshot, client-side only)
72
+ * const snap = ProductState.read();
58
73
  * ```
59
74
  */
60
75
  export function createLocationState<TState>(
61
- key?: string
76
+ options?: LocationStateOptions,
62
77
  ): LocationStateDefinition<[TState | (() => TState)], TState> {
63
- if (!key && process.env.NODE_ENV !== "production") {
64
- console.warn(
65
- "[rsc-router] createLocationState is missing a key. " +
66
- "Make sure the exposeLocationStateId Vite plugin is enabled and " +
67
- "the state is exported with: export const MyState = createLocationState(...)"
68
- );
69
- }
70
- const fullKey = `__rsc_ls_${key}`;
78
+ const flash = options?.flash ?? false;
79
+ let _key: string | undefined;
71
80
 
72
- // Warn about duplicate keys in development
73
- if (process.env.NODE_ENV !== "production" && usedKeys.has(fullKey)) {
74
- console.warn(
75
- `[rsc-router] Duplicate location state key "${key}". ` +
76
- `Each createLocationState call should have a unique key.`
77
- );
81
+ function getKey(): string {
82
+ if (!_key && process.env.NODE_ENV === "development") {
83
+ throw new Error(
84
+ "[rsc-router] createLocationState key not set. " +
85
+ "Make sure the exposeInternalIds Vite plugin is enabled and " +
86
+ "the state is exported with: export const MyState = createLocationState(...)",
87
+ );
88
+ }
89
+ return _key!;
78
90
  }
79
- usedKeys.add(fullKey);
80
91
 
81
- const definition = Object.assign(
82
- (stateOrGetter: TState | (() => TState)): LocationStateEntry => ({
83
- __rsc_ls_key: fullKey,
84
- // Resolve getter immediately - lazy evaluation happens via Link's stateRef pattern
85
- __rsc_ls_value:
86
- typeof stateOrGetter === "function"
87
- ? (stateOrGetter as () => TState)()
88
- : stateOrGetter,
89
- }),
90
- { __rsc_ls_key: fullKey }
91
- );
92
+ const fn = (stateOrGetter: TState | (() => TState)): LocationStateEntry => {
93
+ if (typeof stateOrGetter === "function") {
94
+ // Store getter as-is; resolved at navigation time by resolveLocationStateEntries()
95
+ return {
96
+ __rsc_ls_key: getKey(),
97
+ __rsc_ls_value: stateOrGetter,
98
+ __rsc_ls_lazy: true,
99
+ };
100
+ }
101
+ return {
102
+ __rsc_ls_key: getKey(),
103
+ __rsc_ls_value: stateOrGetter,
104
+ };
105
+ };
106
+
107
+ // Use defineProperty for __rsc_ls_key to avoid Object.assign evaluating
108
+ // the getter during construction (before the Vite plugin sets the key).
109
+ Object.defineProperty(fn, "__rsc_ls_key", {
110
+ get: () => getKey(),
111
+ set: (k: string) => {
112
+ _key = k;
113
+ },
114
+ enumerable: true,
115
+ configurable: true,
116
+ });
117
+
118
+ Object.defineProperty(fn, "__rsc_ls_flash", {
119
+ value: flash,
120
+ enumerable: true,
121
+ });
122
+
123
+ Object.defineProperty(fn, "read", {
124
+ value: (): TState | undefined => {
125
+ if (typeof window === "undefined") return undefined;
126
+ return window.history.state?.[getKey()] as TState | undefined;
127
+ },
128
+ enumerable: true,
129
+ });
92
130
 
93
- return definition as LocationStateDefinition<[TState | (() => TState)], TState>;
131
+ return fn as LocationStateDefinition<[TState | (() => TState)], TState>;
94
132
  }
95
133
 
96
134
  /**
97
135
  * Check if a value is a LocationStateEntry
98
136
  */
99
- export function isLocationStateEntry(value: unknown): value is LocationStateEntry {
137
+ export function isLocationStateEntry(
138
+ value: unknown,
139
+ ): value is LocationStateEntry {
100
140
  return (
101
141
  value !== null &&
102
142
  typeof value === "object" &&
@@ -110,11 +150,13 @@ export function isLocationStateEntry(value: unknown): value is LocationStateEntr
110
150
  * Resolve state entries into a flat object for history.state
111
151
  */
112
152
  export function resolveLocationStateEntries(
113
- entries: LocationStateEntry[]
153
+ entries: LocationStateEntry[],
114
154
  ): Record<string, unknown> {
115
155
  const result: Record<string, unknown> = {};
116
156
  for (const entry of entries) {
117
- result[entry.__rsc_ls_key] = entry.__rsc_ls_value;
157
+ result[entry.__rsc_ls_key] = entry.__rsc_ls_lazy
158
+ ? (entry.__rsc_ls_value as () => unknown)()
159
+ : entry.__rsc_ls_value;
118
160
  }
119
161
  return result;
120
162
  }
@@ -10,53 +10,98 @@ export {
10
10
  resolveLocationStateEntries,
11
11
  type LocationStateEntry,
12
12
  type LocationStateDefinition,
13
+ type LocationStateOptions,
13
14
  } from "./location-state-shared.js";
14
15
 
15
16
  /**
16
17
  * Hook to read location state from history.state
17
18
  *
19
+ * Behavior depends on the definition:
20
+ * - Normal state: persists across navigations, reactive to popstate
21
+ * - Flash state (created with { flash: true }): read once, cleared after paint
22
+ *
18
23
  * Overloaded:
19
24
  * - With definition: Returns typed state from the specific key
20
- * - With type param only: Returns legacy state from history.state.state (backwards compat)
25
+ * - With type param only: Returns plain state from history.state.state
21
26
  *
22
27
  * @example
23
28
  * ```typescript
24
- * // Typed access with definition (recommended)
25
- * const ProductState = createLocationState<{ name: string }>("product");
29
+ * // Persistent state
30
+ * const ProductState = createLocationState<{ name: string }>();
26
31
  * const state = useLocationState(ProductState);
27
- * // state: { name: string } | undefined
28
32
  *
29
- * // Legacy typed access (backwards compatible)
30
- * const legacyState = useLocationState<{ from?: string }>();
33
+ * // Flash state (auto-clears after paint)
34
+ * const FlashMsg = createLocationState<{ text: string }>({ flash: true });
35
+ * const flash = useLocationState(FlashMsg);
36
+ *
37
+ * // Plain state access (reads from history.state.state)
38
+ * const state = useLocationState<{ from?: string }>();
31
39
  * ```
32
40
  */
33
41
  export function useLocationState<TArgs extends unknown[], TState>(
34
- definition: LocationStateDefinition<TArgs, TState>
42
+ definition: LocationStateDefinition<TArgs, TState>,
35
43
  ): TState | undefined;
36
44
  export function useLocationState<T = unknown>(): T | undefined;
37
45
  export function useLocationState<TArgs extends unknown[], TState>(
38
- definition?: LocationStateDefinition<TArgs, TState>
46
+ definition?: LocationStateDefinition<TArgs, TState>,
39
47
  ): TState | undefined {
48
+ const key = definition?.__rsc_ls_key;
49
+ const isFlash = definition?.__rsc_ls_flash ?? false;
50
+
40
51
  const [state, setState] = useState<TState | undefined>(() => {
41
52
  if (typeof window === "undefined") return undefined;
42
- if (definition) {
43
- return window.history.state?.[definition.__rsc_ls_key] as TState | undefined;
53
+ if (key) {
54
+ return window.history.state?.[key] as TState | undefined;
44
55
  }
45
- // Legacy: return history.state.state for backwards compatibility
56
+ // Plain state: stored under history.state.state
46
57
  return window.history.state?.state as TState | undefined;
47
58
  });
48
59
 
60
+ // Subscribe to popstate and programmatic state changes
49
61
  useEffect(() => {
50
62
  const handlePopstate = () => {
51
- if (definition) {
52
- setState(window.history.state?.[definition.__rsc_ls_key] as TState | undefined);
63
+ if (key) {
64
+ setState(window.history.state?.[key] as TState | undefined);
53
65
  } else {
54
66
  setState(window.history.state?.state as TState | undefined);
55
67
  }
56
68
  };
69
+
70
+ // Handle programmatic state changes (same-page navigation with
71
+ // ctx.setLocationState where components don't remount)
72
+ const handleLocationState = () => {
73
+ if (key) {
74
+ const val = window.history.state?.[key] as TState | undefined;
75
+ if (isFlash) {
76
+ // For flash state, only update if there's a new value
77
+ if (val !== undefined) {
78
+ setState(val);
79
+ }
80
+ } else {
81
+ setState(val);
82
+ }
83
+ } else {
84
+ setState(window.history.state?.state as TState | undefined);
85
+ }
86
+ };
87
+
57
88
  window.addEventListener("popstate", handlePopstate);
58
- return () => window.removeEventListener("popstate", handlePopstate);
59
- }, [definition]);
89
+ window.addEventListener("__rsc_locationstate", handleLocationState);
90
+ return () => {
91
+ window.removeEventListener("popstate", handlePopstate);
92
+ window.removeEventListener("__rsc_locationstate", handleLocationState);
93
+ };
94
+ }, [key, isFlash]);
95
+
96
+ // Flash: clear from history.state after paint so subsequent navigations don't see it.
97
+ // Depends on `state` so it re-runs when state is set via the event listener.
98
+ useEffect(() => {
99
+ if (isFlash && key && state !== undefined) {
100
+ const cleaned = { ...window.history.state };
101
+ delete cleaned[key];
102
+ window.history.replaceState(cleaned, "", window.location.href);
103
+ }
104
+ }, [isFlash, key, state]);
60
105
 
61
106
  return state;
62
107
  }