@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.81

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 (316) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +5091 -941
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +61 -52
  7. package/skills/breadcrumbs/SKILL.md +250 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +167 -0
  14. package/skills/handler-use/SKILL.md +362 -0
  15. package/skills/hooks/SKILL.md +340 -72
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/intercept/SKILL.md +151 -8
  18. package/skills/layout/SKILL.md +122 -3
  19. package/skills/links/SKILL.md +92 -31
  20. package/skills/loader/SKILL.md +404 -44
  21. package/skills/middleware/SKILL.md +205 -37
  22. package/skills/migrate-nextjs/SKILL.md +560 -0
  23. package/skills/migrate-react-router/SKILL.md +765 -0
  24. package/skills/mime-routes/SKILL.md +128 -0
  25. package/skills/parallel/SKILL.md +263 -1
  26. package/skills/prerender/SKILL.md +685 -0
  27. package/skills/rango/SKILL.md +87 -16
  28. package/skills/response-routes/SKILL.md +411 -0
  29. package/skills/route/SKILL.md +281 -14
  30. package/skills/router-setup/SKILL.md +210 -32
  31. package/skills/tailwind/SKILL.md +129 -0
  32. package/skills/theme/SKILL.md +9 -8
  33. package/skills/typesafety/SKILL.md +328 -89
  34. package/skills/use-cache/SKILL.md +324 -0
  35. package/src/__internal.ts +102 -4
  36. package/src/bin/rango.ts +321 -0
  37. package/src/browser/action-coordinator.ts +97 -0
  38. package/src/browser/action-response-classifier.ts +99 -0
  39. package/src/browser/app-version.ts +14 -0
  40. package/src/browser/event-controller.ts +92 -64
  41. package/src/browser/history-state.ts +80 -0
  42. package/src/browser/intercept-utils.ts +52 -0
  43. package/src/browser/link-interceptor.ts +24 -4
  44. package/src/browser/logging.ts +55 -0
  45. package/src/browser/merge-segment-loaders.ts +20 -12
  46. package/src/browser/navigation-bridge.ts +317 -560
  47. package/src/browser/navigation-client.ts +206 -68
  48. package/src/browser/navigation-store.ts +73 -55
  49. package/src/browser/navigation-transaction.ts +297 -0
  50. package/src/browser/network-error-handler.ts +61 -0
  51. package/src/browser/partial-update.ts +343 -316
  52. package/src/browser/prefetch/cache.ts +216 -0
  53. package/src/browser/prefetch/fetch.ts +206 -0
  54. package/src/browser/prefetch/observer.ts +65 -0
  55. package/src/browser/prefetch/policy.ts +48 -0
  56. package/src/browser/prefetch/queue.ts +160 -0
  57. package/src/browser/prefetch/resource-ready.ts +77 -0
  58. package/src/browser/rango-state.ts +112 -0
  59. package/src/browser/react/Link.tsx +253 -74
  60. package/src/browser/react/NavigationProvider.tsx +91 -11
  61. package/src/browser/react/context.ts +11 -0
  62. package/src/browser/react/filter-segment-order.ts +11 -0
  63. package/src/browser/react/index.ts +12 -12
  64. package/src/browser/react/location-state-shared.ts +95 -53
  65. package/src/browser/react/location-state.ts +60 -15
  66. package/src/browser/react/mount-context.ts +6 -1
  67. package/src/browser/react/nonce-context.ts +23 -0
  68. package/src/browser/react/shallow-equal.ts +27 -0
  69. package/src/browser/react/use-action.ts +29 -51
  70. package/src/browser/react/use-client-cache.ts +5 -3
  71. package/src/browser/react/use-handle.ts +30 -126
  72. package/src/browser/react/use-href.tsx +2 -2
  73. package/src/browser/react/use-link-status.ts +6 -5
  74. package/src/browser/react/use-navigation.ts +44 -65
  75. package/src/browser/react/use-params.ts +75 -0
  76. package/src/browser/react/use-pathname.ts +47 -0
  77. package/src/browser/react/use-router.ts +76 -0
  78. package/src/browser/react/use-search-params.ts +56 -0
  79. package/src/browser/react/use-segments.ts +80 -97
  80. package/src/browser/response-adapter.ts +73 -0
  81. package/src/browser/rsc-router.tsx +214 -58
  82. package/src/browser/scroll-restoration.ts +127 -52
  83. package/src/browser/segment-reconciler.ts +243 -0
  84. package/src/browser/segment-structure-assert.ts +16 -0
  85. package/src/browser/server-action-bridge.ts +510 -603
  86. package/src/browser/shallow.ts +6 -1
  87. package/src/browser/types.ts +141 -48
  88. package/src/browser/validate-redirect-origin.ts +29 -0
  89. package/src/build/generate-manifest.ts +235 -24
  90. package/src/build/generate-route-types.ts +39 -0
  91. package/src/build/index.ts +13 -0
  92. package/src/build/route-trie.ts +291 -0
  93. package/src/build/route-types/ast-helpers.ts +25 -0
  94. package/src/build/route-types/ast-route-extraction.ts +98 -0
  95. package/src/build/route-types/codegen.ts +102 -0
  96. package/src/build/route-types/include-resolution.ts +418 -0
  97. package/src/build/route-types/param-extraction.ts +48 -0
  98. package/src/build/route-types/per-module-writer.ts +128 -0
  99. package/src/build/route-types/router-processing.ts +618 -0
  100. package/src/build/route-types/scan-filter.ts +85 -0
  101. package/src/build/runtime-discovery.ts +231 -0
  102. package/src/cache/background-task.ts +34 -0
  103. package/src/cache/cache-key-utils.ts +44 -0
  104. package/src/cache/cache-policy.ts +125 -0
  105. package/src/cache/cache-runtime.ts +342 -0
  106. package/src/cache/cache-scope.ts +167 -309
  107. package/src/cache/cf/cf-cache-store.ts +571 -17
  108. package/src/cache/cf/index.ts +13 -3
  109. package/src/cache/document-cache.ts +116 -77
  110. package/src/cache/handle-capture.ts +81 -0
  111. package/src/cache/handle-snapshot.ts +41 -0
  112. package/src/cache/index.ts +1 -15
  113. package/src/cache/memory-segment-store.ts +191 -13
  114. package/src/cache/profile-registry.ts +73 -0
  115. package/src/cache/read-through-swr.ts +134 -0
  116. package/src/cache/segment-codec.ts +256 -0
  117. package/src/cache/taint.ts +153 -0
  118. package/src/cache/types.ts +72 -122
  119. package/src/client.rsc.tsx +3 -1
  120. package/src/client.tsx +135 -301
  121. package/src/component-utils.ts +4 -4
  122. package/src/components/DefaultDocument.tsx +5 -1
  123. package/src/context-var.ts +156 -0
  124. package/src/debug.ts +19 -9
  125. package/src/errors.ts +108 -2
  126. package/src/handle.ts +55 -29
  127. package/src/handles/MetaTags.tsx +73 -20
  128. package/src/handles/breadcrumbs.ts +66 -0
  129. package/src/handles/index.ts +1 -0
  130. package/src/handles/meta.ts +30 -13
  131. package/src/host/cookie-handler.ts +21 -15
  132. package/src/host/errors.ts +8 -8
  133. package/src/host/index.ts +4 -7
  134. package/src/host/pattern-matcher.ts +27 -27
  135. package/src/host/router.ts +61 -39
  136. package/src/host/testing.ts +8 -8
  137. package/src/host/types.ts +15 -7
  138. package/src/host/utils.ts +1 -1
  139. package/src/href-client.ts +119 -29
  140. package/src/index.rsc.ts +155 -19
  141. package/src/index.ts +251 -30
  142. package/src/internal-debug.ts +11 -0
  143. package/src/loader.rsc.ts +26 -157
  144. package/src/loader.ts +27 -10
  145. package/src/network-error-thrower.tsx +3 -1
  146. package/src/outlet-provider.tsx +45 -0
  147. package/src/prerender/param-hash.ts +37 -0
  148. package/src/prerender/store.ts +186 -0
  149. package/src/prerender.ts +524 -0
  150. package/src/reverse.ts +354 -0
  151. package/src/root-error-boundary.tsx +41 -29
  152. package/src/route-content-wrapper.tsx +7 -4
  153. package/src/route-definition/dsl-helpers.ts +1121 -0
  154. package/src/route-definition/helper-factories.ts +200 -0
  155. package/src/route-definition/helpers-types.ts +478 -0
  156. package/src/route-definition/index.ts +55 -0
  157. package/src/route-definition/redirect.ts +101 -0
  158. package/src/route-definition/resolve-handler-use.ts +149 -0
  159. package/src/route-definition.ts +1 -1428
  160. package/src/route-map-builder.ts +217 -123
  161. package/src/route-name.ts +53 -0
  162. package/src/route-types.ts +77 -8
  163. package/src/router/content-negotiation.ts +215 -0
  164. package/src/router/debug-manifest.ts +72 -0
  165. package/src/router/error-handling.ts +9 -9
  166. package/src/router/find-match.ts +160 -0
  167. package/src/router/handler-context.ts +438 -86
  168. package/src/router/intercept-resolution.ts +402 -0
  169. package/src/router/lazy-includes.ts +237 -0
  170. package/src/router/loader-resolution.ts +356 -128
  171. package/src/router/logging.ts +251 -0
  172. package/src/router/manifest.ts +163 -35
  173. package/src/router/match-api.ts +555 -0
  174. package/src/router/match-context.ts +5 -3
  175. package/src/router/match-handlers.ts +440 -0
  176. package/src/router/match-middleware/background-revalidation.ts +108 -93
  177. package/src/router/match-middleware/cache-lookup.ts +460 -10
  178. package/src/router/match-middleware/cache-store.ts +98 -26
  179. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  180. package/src/router/match-middleware/segment-resolution.ts +80 -6
  181. package/src/router/match-pipelines.ts +10 -45
  182. package/src/router/match-result.ts +135 -35
  183. package/src/router/metrics.ts +240 -15
  184. package/src/router/middleware-cookies.ts +55 -0
  185. package/src/router/middleware-types.ts +220 -0
  186. package/src/router/middleware.ts +324 -369
  187. package/src/router/navigation-snapshot.ts +182 -0
  188. package/src/router/pattern-matching.ts +211 -43
  189. package/src/router/prerender-match.ts +502 -0
  190. package/src/router/preview-match.ts +98 -0
  191. package/src/router/request-classification.ts +310 -0
  192. package/src/router/revalidation.ts +137 -38
  193. package/src/router/route-snapshot.ts +245 -0
  194. package/src/router/router-context.ts +41 -21
  195. package/src/router/router-interfaces.ts +484 -0
  196. package/src/router/router-options.ts +618 -0
  197. package/src/router/router-registry.ts +24 -0
  198. package/src/router/segment-resolution/fresh.ts +748 -0
  199. package/src/router/segment-resolution/helpers.ts +268 -0
  200. package/src/router/segment-resolution/loader-cache.ts +199 -0
  201. package/src/router/segment-resolution/revalidation.ts +1379 -0
  202. package/src/router/segment-resolution/static-store.ts +67 -0
  203. package/src/router/segment-resolution.ts +21 -0
  204. package/src/router/segment-wrappers.ts +291 -0
  205. package/src/router/telemetry-otel.ts +299 -0
  206. package/src/router/telemetry.ts +300 -0
  207. package/src/router/timeout.ts +148 -0
  208. package/src/router/trie-matching.ts +239 -0
  209. package/src/router/types.ts +78 -3
  210. package/src/router.ts +740 -4252
  211. package/src/rsc/handler-context.ts +45 -0
  212. package/src/rsc/handler.ts +907 -797
  213. package/src/rsc/helpers.ts +140 -6
  214. package/src/rsc/index.ts +0 -20
  215. package/src/rsc/loader-fetch.ts +229 -0
  216. package/src/rsc/manifest-init.ts +90 -0
  217. package/src/rsc/nonce.ts +14 -0
  218. package/src/rsc/origin-guard.ts +141 -0
  219. package/src/rsc/progressive-enhancement.ts +393 -0
  220. package/src/rsc/response-error.ts +37 -0
  221. package/src/rsc/response-route-handler.ts +347 -0
  222. package/src/rsc/rsc-rendering.ts +246 -0
  223. package/src/rsc/runtime-warnings.ts +42 -0
  224. package/src/rsc/server-action.ts +358 -0
  225. package/src/rsc/ssr-setup.ts +128 -0
  226. package/src/rsc/types.ts +46 -11
  227. package/src/search-params.ts +230 -0
  228. package/src/segment-content-promise.ts +67 -0
  229. package/src/segment-loader-promise.ts +122 -0
  230. package/src/segment-system.tsx +134 -36
  231. package/src/server/context.ts +341 -61
  232. package/src/server/cookie-store.ts +190 -0
  233. package/src/server/fetchable-loader-store.ts +37 -0
  234. package/src/server/handle-store.ts +113 -15
  235. package/src/server/loader-registry.ts +24 -64
  236. package/src/server/request-context.ts +607 -81
  237. package/src/server.ts +35 -130
  238. package/src/ssr/index.tsx +103 -30
  239. package/src/static-handler.ts +126 -0
  240. package/src/theme/ThemeProvider.tsx +21 -15
  241. package/src/theme/ThemeScript.tsx +5 -5
  242. package/src/theme/constants.ts +5 -2
  243. package/src/theme/index.ts +4 -14
  244. package/src/theme/theme-context.ts +4 -30
  245. package/src/theme/theme-script.ts +21 -18
  246. package/src/types/boundaries.ts +158 -0
  247. package/src/types/cache-types.ts +198 -0
  248. package/src/types/error-types.ts +192 -0
  249. package/src/types/global-namespace.ts +100 -0
  250. package/src/types/handler-context.ts +791 -0
  251. package/src/types/index.ts +88 -0
  252. package/src/types/loader-types.ts +210 -0
  253. package/src/types/route-config.ts +170 -0
  254. package/src/types/route-entry.ts +120 -0
  255. package/src/types/segments.ts +150 -0
  256. package/src/types.ts +1 -1623
  257. package/src/urls/include-helper.ts +207 -0
  258. package/src/urls/index.ts +53 -0
  259. package/src/urls/path-helper-types.ts +372 -0
  260. package/src/urls/path-helper.ts +364 -0
  261. package/src/urls/pattern-types.ts +107 -0
  262. package/src/urls/response-types.ts +116 -0
  263. package/src/urls/type-extraction.ts +372 -0
  264. package/src/urls/urls-function.ts +98 -0
  265. package/src/urls.ts +1 -802
  266. package/src/use-loader.tsx +161 -81
  267. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  268. package/src/vite/discovery/discover-routers.ts +348 -0
  269. package/src/vite/discovery/prerender-collection.ts +439 -0
  270. package/src/vite/discovery/route-types-writer.ts +258 -0
  271. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  272. package/src/vite/discovery/state.ts +117 -0
  273. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  274. package/src/vite/index.ts +15 -1133
  275. package/src/vite/plugin-types.ts +103 -0
  276. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  277. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  278. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  279. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  280. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  281. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  282. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  283. package/src/vite/plugins/expose-id-utils.ts +299 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  290. package/src/vite/plugins/performance-tracks.ts +88 -0
  291. package/src/vite/plugins/refresh-cmd.ts +127 -0
  292. package/src/vite/plugins/use-cache-transform.ts +323 -0
  293. package/src/vite/plugins/version-injector.ts +83 -0
  294. package/src/vite/plugins/version-plugin.ts +266 -0
  295. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +462 -0
  298. package/src/vite/router-discovery.ts +977 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  304. package/src/vite/utils/prerender-utils.ts +221 -0
  305. package/src/vite/utils/shared-utils.ts +170 -0
  306. package/CLAUDE.md +0 -43
  307. package/src/browser/lru-cache.ts +0 -69
  308. package/src/browser/request-controller.ts +0 -164
  309. package/src/cache/memory-store.ts +0 -253
  310. package/src/href-context.ts +0 -33
  311. package/src/href.ts +0 -255
  312. package/src/server/route-manifest-cache.ts +0 -173
  313. package/src/vite/expose-handle-id.ts +0 -209
  314. package/src/vite/expose-loader-id.ts +0 -426
  315. package/src/vite/expose-location-state-id.ts +0 -177
  316. /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,7 +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";
29
+ import { cancelAllPrefetches } from "../prefetch/queue.js";
30
+ import { handleNavigationEnd } from "../scroll-restoration.js";
26
31
 
27
32
  /**
28
33
  * Process handles from an async generator, updating the event controller
@@ -42,7 +47,7 @@ async function processHandles(
42
47
  matched?: string[];
43
48
  isPartial?: boolean;
44
49
  historyKey: string;
45
- }
50
+ },
46
51
  ): Promise<void> {
47
52
  const { eventController, store, matched, isPartial, historyKey } = opts;
48
53
 
@@ -53,7 +58,7 @@ async function processHandles(
53
58
  // the current route's breadcrumbs (e.g., quick popstate after clicking a link).
54
59
  if (historyKey !== store.getHistoryKey()) {
55
60
  console.log(
56
- "[NavigationProvider] Stopping handle processing - user navigated away"
61
+ "[NavigationProvider] Stopping handle processing - user navigated away",
57
62
  );
58
63
  return;
59
64
  }
@@ -100,9 +105,9 @@ export interface NavigationProviderProps {
100
105
  eventController: EventController;
101
106
 
102
107
  /**
103
- * Initial RSC payload from server
108
+ * Initial rendered tree + metadata from server payload
104
109
  */
105
- initialPayload: RscPayload;
110
+ initialPayload: NavigationUpdate;
106
111
 
107
112
  /**
108
113
  * Navigation bridge for handling navigation
@@ -126,6 +131,17 @@ export interface NavigationProviderProps {
126
131
  * When true, keeps TLS alive by sending HEAD requests after idle periods.
127
132
  */
128
133
  warmupEnabled?: boolean;
134
+
135
+ /**
136
+ * App version from server payload (stable, immutable).
137
+ * Forwarded to context for cache key building.
138
+ */
139
+ version?: string;
140
+
141
+ /**
142
+ * URL prefix for all routes (from createRouter({ basename })).
143
+ */
144
+ basename?: string;
129
145
  }
130
146
 
131
147
  /**
@@ -157,6 +173,8 @@ export function NavigationProvider({
157
173
  themeConfig,
158
174
  initialTheme,
159
175
  warmupEnabled,
176
+ version,
177
+ basename,
160
178
  }: NavigationProviderProps): ReactNode {
161
179
  // Track current payload for rendering (this triggers re-renders)
162
180
  const [payload, setPayload] = useState(initialPayload);
@@ -168,7 +186,7 @@ export function NavigationProvider({
168
186
  async (url: string, options?: NavigateOptions): Promise<void> => {
169
187
  await bridge.navigate(url, options);
170
188
  },
171
- []
189
+ [],
172
190
  );
173
191
 
174
192
  /**
@@ -185,8 +203,10 @@ export function NavigationProvider({
185
203
  eventController,
186
204
  navigate,
187
205
  refresh,
206
+ version,
207
+ basename,
188
208
  }),
189
- []
209
+ [],
190
210
  );
191
211
 
192
212
  // Connection warmup: keep TLS alive after idle periods.
@@ -252,7 +272,12 @@ export function NavigationProvider({
252
272
  }
253
273
 
254
274
  // Activity events that reset the idle timer
255
- const activityEvents = ["mousemove", "keydown", "touchstart", "scroll"] as const;
275
+ const activityEvents = [
276
+ "mousemove",
277
+ "keydown",
278
+ "touchstart",
279
+ "scroll",
280
+ ] as const;
256
281
  const activityOptions: AddEventListenerOptions = { passive: true };
257
282
 
258
283
  for (const event of activityEvents) {
@@ -271,14 +296,62 @@ export function NavigationProvider({
271
296
  };
272
297
  }, [warmupEnabled]);
273
298
 
299
+ // Cancel non-matching prefetches when navigation starts.
300
+ // Frees connections so the navigation fetch isn't competing with
301
+ // speculative prefetches. The prefetch matching the navigation target
302
+ // is kept alive so it can be reused via consumeInflightPrefetch.
303
+ useEffect(() => {
304
+ let wasIdle = true;
305
+ const unsub = eventController.subscribe(() => {
306
+ const state = eventController.getState();
307
+ const isIdle = state.state === "idle" && !state.isStreaming;
308
+ if (wasIdle && !isIdle) {
309
+ cancelAllPrefetches(state.pendingUrl);
310
+ }
311
+ wasIdle = isIdle;
312
+ });
313
+ return unsub;
314
+ }, [eventController]);
315
+
316
+ // Pending scroll action to apply after React commits
317
+ const pendingScrollRef = useRef<NavigationUpdate["scroll"]>(undefined);
318
+
319
+ // Apply scroll after React commits the new content to the DOM
320
+ useLayoutEffect(() => {
321
+ const scrollAction = pendingScrollRef.current;
322
+ if (!scrollAction) return;
323
+ pendingScrollRef.current = undefined;
324
+
325
+ if (scrollAction.enabled === false) return;
326
+
327
+ handleNavigationEnd({
328
+ restore: scrollAction.restore,
329
+ scroll: scrollAction.enabled,
330
+ isStreaming: scrollAction.isStreaming,
331
+ });
332
+ });
333
+
274
334
  // Subscribe to UI updates (for re-rendering the tree)
275
335
  useEffect(() => {
276
336
  const unsubscribe = store.onUpdate((update) => {
337
+ // Capture scroll intent — it will be applied in useLayoutEffect
338
+ // after React commits this state update to the DOM.
339
+ // Always assign (even undefined) to clear stale scroll from prior navigations,
340
+ // so server actions or error updates don't accidentally replay old scroll.
341
+ pendingScrollRef.current = update.scroll;
342
+
277
343
  setPayload({
278
344
  root: update.root,
279
345
  metadata: update.metadata,
280
346
  });
281
347
 
348
+ // Update route params. Only reset when the server actually sends a params
349
+ // map — an absent `params` field means "no change" (e.g., legacy action
350
+ // responses that omitted params). Explicit `{}` still clears correctly.
351
+ if (update.metadata.params !== undefined) {
352
+ eventController.setParams(update.metadata.params);
353
+ }
354
+
282
355
  // Update handle data progressively as it streams in
283
356
  if (update.metadata.handles) {
284
357
  // Capture historyKey now - by the time async processing completes,
@@ -292,7 +365,7 @@ export function NavigationProvider({
292
365
  isPartial: update.metadata.isPartial,
293
366
  historyKey,
294
367
  }).catch((err) =>
295
- console.error("[NavigationProvider] Error consuming handles:", err)
368
+ console.error("[NavigationProvider] Error consuming handles:", err),
296
369
  );
297
370
  } else if (update.metadata.cachedHandleData) {
298
371
  // For back/forward navigation from cache, restore the cached handleData
@@ -300,14 +373,14 @@ export function NavigationProvider({
300
373
  eventController.setHandleData(
301
374
  update.metadata.cachedHandleData,
302
375
  update.metadata.matched,
303
- false // full replace - restore entire cached state
376
+ false, // full replace - restore entire cached state
304
377
  );
305
378
  } else if (update.metadata.matched) {
306
379
  // For cached navigations without handleData, update segmentOrder to clean up stale data
307
380
  eventController.setHandleData(
308
381
  {}, // Empty data - all existing data not in matched will be cleaned up
309
382
  update.metadata.matched,
310
- true // partial update - will clean up segments not in matched
383
+ true, // partial update - will clean up segments not in matched
311
384
  );
312
385
  }
313
386
  });
@@ -338,6 +411,13 @@ export function NavigationProvider({
338
411
  );
339
412
  }
340
413
 
414
+ // Match SSR tree shape: NonceContext.Provider is always present so
415
+ // hydration sees the same component tree. Value is undefined on the
416
+ // client — CSP nonces are a server-side HTML concern.
417
+ content = (
418
+ <NonceContext.Provider value={undefined}>{content}</NonceContext.Provider>
419
+ );
420
+
341
421
  return (
342
422
  <NavigationStoreContext.Provider value={contextValue}>
343
423
  {content}
@@ -41,6 +41,17 @@ 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 the initial server payload.
47
+ */
48
+ version: string | undefined;
49
+
50
+ /**
51
+ * URL prefix for all routes (from createRouter({ basename })).
52
+ * Used by Link and useRouter() to auto-prefix app-local paths.
53
+ */
54
+ basename: string | undefined;
44
55
  }
45
56
 
46
57
  /**
@@ -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
  }
@@ -1,6 +1,11 @@
1
1
  "use client";
2
2
 
3
- import { createContext, createElement, type Context, type ReactNode } from "react";
3
+ import {
4
+ createContext,
5
+ createElement,
6
+ type Context,
7
+ type ReactNode,
8
+ } from "react";
4
9
 
5
10
  /**
6
11
  * Context for the current include() mount path.
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Context for CSP nonce propagation to client components during SSR.
5
+ *
6
+ * The SSR renderer wraps the tree with NonceContext.Provider so that
7
+ * client components (e.g. MetaTags) can apply nonces to inline scripts.
8
+ * On the browser side, no provider is needed — the default undefined
9
+ * is correct since CSP nonces are a server-side HTML concern.
10
+ */
11
+
12
+ import { createContext, useContext, type Context } from "react";
13
+
14
+ export const NonceContext: Context<string | undefined> = createContext<
15
+ string | undefined
16
+ >(undefined);
17
+
18
+ /**
19
+ * Read the CSP nonce during SSR. Returns undefined on the client.
20
+ */
21
+ export function useNonce(): string | undefined {
22
+ return useContext(NonceContext);
23
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shallow equality check for selector results.
3
+ * Uses Object.is for value comparison (handles NaN and +-0 correctly).
4
+ */
5
+ export function shallowEqual<T>(a: T, b: T): boolean {
6
+ if (Object.is(a, b)) return true;
7
+ if (
8
+ typeof a !== "object" ||
9
+ a === null ||
10
+ typeof b !== "object" ||
11
+ b === null
12
+ ) {
13
+ return false;
14
+ }
15
+ const keysA = Object.keys(a);
16
+ const keysB = Object.keys(b);
17
+ if (keysA.length !== keysB.length) return false;
18
+ for (const key of keysA) {
19
+ if (
20
+ !Object.hasOwn(b, key) ||
21
+ !Object.is((a as any)[key], (b as any)[key])
22
+ ) {
23
+ return false;
24
+ }
25
+ }
26
+ return true;
27
+ }