@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100

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 (329) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +1037 -4
  3. package/dist/bin/rango.js +1619 -157
  4. package/dist/vite/index.js +5762 -2301
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +71 -63
  7. package/skills/breadcrumbs/SKILL.md +252 -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 +6 -4
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +367 -71
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +176 -8
  19. package/skills/layout/SKILL.md +124 -3
  20. package/skills/links/SKILL.md +304 -25
  21. package/skills/loader/SKILL.md +474 -47
  22. package/skills/middleware/SKILL.md +207 -37
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +15 -11
  26. package/skills/parallel/SKILL.md +272 -1
  27. package/skills/prerender/SKILL.md +467 -65
  28. package/skills/rango/SKILL.md +89 -21
  29. package/skills/response-routes/SKILL.md +152 -91
  30. package/skills/route/SKILL.md +305 -14
  31. package/skills/router-setup/SKILL.md +210 -32
  32. package/skills/server-actions/SKILL.md +739 -0
  33. package/skills/streams-and-websockets/SKILL.md +283 -0
  34. package/skills/theme/SKILL.md +9 -8
  35. package/skills/typesafety/SKILL.md +333 -86
  36. package/skills/use-cache/SKILL.md +324 -0
  37. package/skills/view-transitions/SKILL.md +212 -0
  38. package/src/__internal.ts +102 -4
  39. package/src/bin/rango.ts +312 -15
  40. package/src/browser/action-coordinator.ts +97 -0
  41. package/src/browser/action-response-classifier.ts +99 -0
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/app-version.ts +14 -0
  44. package/src/browser/event-controller.ts +136 -68
  45. package/src/browser/history-state.ts +80 -0
  46. package/src/browser/intercept-utils.ts +52 -0
  47. package/src/browser/link-interceptor.ts +24 -4
  48. package/src/browser/logging.ts +55 -0
  49. package/src/browser/merge-segment-loaders.ts +20 -12
  50. package/src/browser/navigation-bridge.ts +374 -561
  51. package/src/browser/navigation-client.ts +228 -70
  52. package/src/browser/navigation-store.ts +97 -55
  53. package/src/browser/navigation-transaction.ts +297 -0
  54. package/src/browser/network-error-handler.ts +61 -0
  55. package/src/browser/partial-update.ts +376 -315
  56. package/src/browser/prefetch/cache.ts +314 -0
  57. package/src/browser/prefetch/fetch.ts +282 -0
  58. package/src/browser/prefetch/observer.ts +65 -0
  59. package/src/browser/prefetch/policy.ts +48 -0
  60. package/src/browser/prefetch/queue.ts +191 -0
  61. package/src/browser/prefetch/resource-ready.ts +77 -0
  62. package/src/browser/rango-state.ts +152 -0
  63. package/src/browser/react/Link.tsx +255 -71
  64. package/src/browser/react/NavigationProvider.tsx +152 -24
  65. package/src/browser/react/context.ts +11 -0
  66. package/src/browser/react/filter-segment-order.ts +55 -0
  67. package/src/browser/react/index.ts +15 -12
  68. package/src/browser/react/location-state-shared.ts +95 -53
  69. package/src/browser/react/location-state.ts +60 -15
  70. package/src/browser/react/mount-context.ts +6 -1
  71. package/src/browser/react/nonce-context.ts +23 -0
  72. package/src/browser/react/shallow-equal.ts +27 -0
  73. package/src/browser/react/use-action.ts +29 -51
  74. package/src/browser/react/use-client-cache.ts +5 -3
  75. package/src/browser/react/use-handle.ts +30 -120
  76. package/src/browser/react/use-link-status.ts +6 -5
  77. package/src/browser/react/use-navigation.ts +44 -65
  78. package/src/browser/react/use-params.ts +78 -0
  79. package/src/browser/react/use-pathname.ts +47 -0
  80. package/src/browser/react/use-reverse.ts +99 -0
  81. package/src/browser/react/use-router.ts +83 -0
  82. package/src/browser/react/use-search-params.ts +56 -0
  83. package/src/browser/react/use-segments.ts +85 -99
  84. package/src/browser/response-adapter.ts +73 -0
  85. package/src/browser/rsc-router.tsx +246 -64
  86. package/src/browser/scroll-restoration.ts +127 -52
  87. package/src/browser/segment-reconciler.ts +243 -0
  88. package/src/browser/segment-structure-assert.ts +16 -0
  89. package/src/browser/server-action-bridge.ts +510 -603
  90. package/src/browser/shallow.ts +6 -1
  91. package/src/browser/types.ts +158 -48
  92. package/src/browser/validate-redirect-origin.ts +29 -0
  93. package/src/build/generate-manifest.ts +84 -23
  94. package/src/build/generate-route-types.ts +39 -828
  95. package/src/build/index.ts +4 -5
  96. package/src/build/route-trie.ts +85 -32
  97. package/src/build/route-types/ast-helpers.ts +25 -0
  98. package/src/build/route-types/ast-route-extraction.ts +98 -0
  99. package/src/build/route-types/codegen.ts +102 -0
  100. package/src/build/route-types/include-resolution.ts +418 -0
  101. package/src/build/route-types/param-extraction.ts +48 -0
  102. package/src/build/route-types/per-module-writer.ts +128 -0
  103. package/src/build/route-types/router-processing.ts +618 -0
  104. package/src/build/route-types/scan-filter.ts +85 -0
  105. package/src/build/runtime-discovery.ts +231 -0
  106. package/src/cache/background-task.ts +34 -0
  107. package/src/cache/cache-key-utils.ts +44 -0
  108. package/src/cache/cache-policy.ts +125 -0
  109. package/src/cache/cache-runtime.ts +342 -0
  110. package/src/cache/cache-scope.ts +167 -307
  111. package/src/cache/cf/cf-cache-store.ts +573 -21
  112. package/src/cache/cf/index.ts +13 -3
  113. package/src/cache/document-cache.ts +116 -77
  114. package/src/cache/handle-capture.ts +81 -0
  115. package/src/cache/handle-snapshot.ts +41 -0
  116. package/src/cache/index.ts +1 -15
  117. package/src/cache/memory-segment-store.ts +191 -13
  118. package/src/cache/profile-registry.ts +73 -0
  119. package/src/cache/read-through-swr.ts +134 -0
  120. package/src/cache/segment-codec.ts +256 -0
  121. package/src/cache/taint.ts +153 -0
  122. package/src/cache/types.ts +72 -122
  123. package/src/client.rsc.tsx +6 -1
  124. package/src/client.tsx +118 -302
  125. package/src/component-utils.ts +4 -4
  126. package/src/components/DefaultDocument.tsx +5 -1
  127. package/src/context-var.ts +156 -0
  128. package/src/debug.ts +19 -9
  129. package/src/errors.ts +77 -7
  130. package/src/handle.ts +55 -10
  131. package/src/handles/MetaTags.tsx +73 -20
  132. package/src/handles/breadcrumbs.ts +66 -0
  133. package/src/handles/index.ts +1 -0
  134. package/src/handles/meta.ts +30 -13
  135. package/src/host/cookie-handler.ts +21 -15
  136. package/src/host/errors.ts +8 -8
  137. package/src/host/index.ts +4 -7
  138. package/src/host/pattern-matcher.ts +27 -27
  139. package/src/host/router.ts +61 -39
  140. package/src/host/testing.ts +8 -8
  141. package/src/host/types.ts +15 -7
  142. package/src/host/utils.ts +1 -1
  143. package/src/href-client.ts +65 -45
  144. package/src/index.rsc.ts +138 -21
  145. package/src/index.ts +206 -51
  146. package/src/internal-debug.ts +11 -0
  147. package/src/loader.rsc.ts +25 -143
  148. package/src/loader.ts +27 -10
  149. package/src/network-error-thrower.tsx +3 -1
  150. package/src/outlet-context.ts +1 -1
  151. package/src/outlet-provider.tsx +45 -0
  152. package/src/prerender/param-hash.ts +4 -2
  153. package/src/prerender/store.ts +159 -13
  154. package/src/prerender.ts +397 -29
  155. package/src/response-utils.ts +28 -0
  156. package/src/reverse.ts +231 -121
  157. package/src/root-error-boundary.tsx +41 -29
  158. package/src/route-content-wrapper.tsx +7 -4
  159. package/src/route-definition/dsl-helpers.ts +1134 -0
  160. package/src/route-definition/helper-factories.ts +200 -0
  161. package/src/route-definition/helpers-types.ts +483 -0
  162. package/src/route-definition/index.ts +55 -0
  163. package/src/route-definition/redirect.ts +101 -0
  164. package/src/route-definition/resolve-handler-use.ts +155 -0
  165. package/src/route-definition.ts +1 -1431
  166. package/src/route-map-builder.ts +162 -123
  167. package/src/route-name.ts +53 -0
  168. package/src/route-types.ts +66 -9
  169. package/src/router/content-negotiation.ts +215 -0
  170. package/src/router/debug-manifest.ts +72 -0
  171. package/src/router/error-handling.ts +9 -9
  172. package/src/router/find-match.ts +160 -0
  173. package/src/router/handler-context.ts +418 -86
  174. package/src/router/intercept-resolution.ts +35 -20
  175. package/src/router/lazy-includes.ts +237 -0
  176. package/src/router/loader-resolution.ts +359 -128
  177. package/src/router/logging.ts +251 -0
  178. package/src/router/manifest.ts +98 -32
  179. package/src/router/match-api.ts +196 -261
  180. package/src/router/match-context.ts +4 -2
  181. package/src/router/match-handlers.ts +441 -0
  182. package/src/router/match-middleware/background-revalidation.ts +108 -93
  183. package/src/router/match-middleware/cache-lookup.ts +415 -86
  184. package/src/router/match-middleware/cache-store.ts +91 -29
  185. package/src/router/match-middleware/intercept-resolution.ts +48 -21
  186. package/src/router/match-middleware/segment-resolution.ts +73 -9
  187. package/src/router/match-pipelines.ts +10 -45
  188. package/src/router/match-result.ts +154 -35
  189. package/src/router/metrics.ts +240 -15
  190. package/src/router/middleware-cookies.ts +55 -0
  191. package/src/router/middleware-types.ts +209 -0
  192. package/src/router/middleware.ts +373 -371
  193. package/src/router/navigation-snapshot.ts +182 -0
  194. package/src/router/pattern-matching.ts +292 -52
  195. package/src/router/prerender-match.ts +502 -0
  196. package/src/router/preview-match.ts +98 -0
  197. package/src/router/request-classification.ts +310 -0
  198. package/src/router/revalidation.ts +152 -39
  199. package/src/router/route-snapshot.ts +245 -0
  200. package/src/router/router-context.ts +41 -21
  201. package/src/router/router-interfaces.ts +484 -0
  202. package/src/router/router-options.ts +618 -0
  203. package/src/router/router-registry.ts +24 -0
  204. package/src/router/segment-resolution/fresh.ts +756 -0
  205. package/src/router/segment-resolution/helpers.ts +268 -0
  206. package/src/router/segment-resolution/loader-cache.ts +199 -0
  207. package/src/router/segment-resolution/revalidation.ts +1407 -0
  208. package/src/router/segment-resolution/static-store.ts +67 -0
  209. package/src/router/segment-resolution.ts +21 -1315
  210. package/src/router/segment-wrappers.ts +291 -0
  211. package/src/router/substitute-pattern-params.ts +56 -0
  212. package/src/router/telemetry-otel.ts +299 -0
  213. package/src/router/telemetry.ts +300 -0
  214. package/src/router/timeout.ts +148 -0
  215. package/src/router/trie-matching.ts +111 -39
  216. package/src/router/types.ts +17 -9
  217. package/src/router/url-params.ts +49 -0
  218. package/src/router.ts +642 -2011
  219. package/src/rsc/handler-context.ts +45 -0
  220. package/src/rsc/handler.ts +864 -1114
  221. package/src/rsc/helpers.ts +181 -19
  222. package/src/rsc/index.ts +0 -20
  223. package/src/rsc/loader-fetch.ts +229 -0
  224. package/src/rsc/manifest-init.ts +90 -0
  225. package/src/rsc/nonce.ts +14 -0
  226. package/src/rsc/origin-guard.ts +141 -0
  227. package/src/rsc/progressive-enhancement.ts +395 -0
  228. package/src/rsc/response-error.ts +37 -0
  229. package/src/rsc/response-route-handler.ts +360 -0
  230. package/src/rsc/rsc-rendering.ts +256 -0
  231. package/src/rsc/runtime-warnings.ts +42 -0
  232. package/src/rsc/server-action.ts +360 -0
  233. package/src/rsc/ssr-setup.ts +128 -0
  234. package/src/rsc/types.ts +52 -11
  235. package/src/search-params.ts +230 -0
  236. package/src/segment-content-promise.ts +67 -0
  237. package/src/segment-loader-promise.ts +122 -0
  238. package/src/segment-system.tsx +187 -38
  239. package/src/server/context.ts +333 -59
  240. package/src/server/cookie-store.ts +190 -0
  241. package/src/server/fetchable-loader-store.ts +37 -0
  242. package/src/server/handle-store.ts +113 -15
  243. package/src/server/loader-registry.ts +24 -64
  244. package/src/server/request-context.ts +603 -109
  245. package/src/server.ts +35 -155
  246. package/src/ssr/index.tsx +107 -30
  247. package/src/static-handler.ts +126 -0
  248. package/src/theme/ThemeProvider.tsx +21 -15
  249. package/src/theme/ThemeScript.tsx +5 -5
  250. package/src/theme/constants.ts +5 -2
  251. package/src/theme/index.ts +4 -14
  252. package/src/theme/theme-context.ts +4 -30
  253. package/src/theme/theme-script.ts +21 -18
  254. package/src/types/boundaries.ts +158 -0
  255. package/src/types/cache-types.ts +198 -0
  256. package/src/types/error-types.ts +192 -0
  257. package/src/types/global-namespace.ts +100 -0
  258. package/src/types/handler-context.ts +764 -0
  259. package/src/types/index.ts +88 -0
  260. package/src/types/loader-types.ts +209 -0
  261. package/src/types/request-scope.ts +126 -0
  262. package/src/types/route-config.ts +170 -0
  263. package/src/types/route-entry.ts +120 -0
  264. package/src/types/segments.ts +167 -0
  265. package/src/types.ts +1 -1757
  266. package/src/urls/include-helper.ts +207 -0
  267. package/src/urls/index.ts +53 -0
  268. package/src/urls/path-helper-types.ts +372 -0
  269. package/src/urls/path-helper.ts +364 -0
  270. package/src/urls/pattern-types.ts +107 -0
  271. package/src/urls/response-types.ts +108 -0
  272. package/src/urls/type-extraction.ts +372 -0
  273. package/src/urls/urls-function.ts +98 -0
  274. package/src/urls.ts +1 -1282
  275. package/src/use-loader.tsx +161 -81
  276. package/src/vite/debug.ts +184 -0
  277. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  278. package/src/vite/discovery/discover-routers.ts +376 -0
  279. package/src/vite/discovery/gate-state.ts +171 -0
  280. package/src/vite/discovery/prerender-collection.ts +486 -0
  281. package/src/vite/discovery/route-types-writer.ts +258 -0
  282. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  283. package/src/vite/discovery/state.ts +117 -0
  284. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  285. package/src/vite/index.ts +15 -2063
  286. package/src/vite/plugin-types.ts +103 -0
  287. package/src/vite/plugins/cjs-to-esm.ts +98 -0
  288. package/src/vite/plugins/client-ref-dedup.ts +131 -0
  289. package/src/vite/plugins/client-ref-hashing.ts +117 -0
  290. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  291. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  292. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  293. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
  294. package/src/vite/plugins/expose-id-utils.ts +299 -0
  295. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  296. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  297. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  298. package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
  299. package/src/vite/plugins/expose-ids/types.ts +45 -0
  300. package/src/vite/plugins/expose-internal-ids.ts +816 -0
  301. package/src/vite/plugins/performance-tracks.ts +96 -0
  302. package/src/vite/plugins/refresh-cmd.ts +127 -0
  303. package/src/vite/plugins/use-cache-transform.ts +336 -0
  304. package/src/vite/plugins/version-injector.ts +109 -0
  305. package/src/vite/plugins/version-plugin.ts +266 -0
  306. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  307. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  308. package/src/vite/rango.ts +497 -0
  309. package/src/vite/router-discovery.ts +1423 -0
  310. package/src/vite/utils/ast-handler-extract.ts +517 -0
  311. package/src/vite/utils/banner.ts +36 -0
  312. package/src/vite/utils/bundle-analysis.ts +137 -0
  313. package/src/vite/utils/manifest-utils.ts +70 -0
  314. package/src/vite/utils/package-resolution.ts +161 -0
  315. package/src/vite/utils/prerender-utils.ts +222 -0
  316. package/src/vite/utils/shared-utils.ts +170 -0
  317. package/CLAUDE.md +0 -43
  318. package/src/browser/lru-cache.ts +0 -69
  319. package/src/browser/request-controller.ts +0 -164
  320. package/src/cache/memory-store.ts +0 -253
  321. package/src/href-context.ts +0 -33
  322. package/src/router.gen.ts +0 -6
  323. package/src/urls.gen.ts +0 -8
  324. package/src/vite/expose-handle-id.ts +0 -209
  325. package/src/vite/expose-loader-id.ts +0 -426
  326. package/src/vite/expose-location-state-id.ts +0 -177
  327. package/src/vite/expose-prerender-handler-id.ts +0 -429
  328. package/src/vite/package-resolution.ts +0 -125
  329. /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,11 @@ 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";
31
+ import type { AppShellRef } from "../app-shell.js";
26
32
 
27
33
  /**
28
34
  * Process handles from an async generator, updating the event controller
@@ -41,10 +47,22 @@ async function processHandles(
41
47
  store: NavigationStore;
42
48
  matched?: string[];
43
49
  isPartial?: boolean;
50
+ /** Server's `resolvedIds`: every segment re-resolved this request,
51
+ * including null-component ones excluded from `diff`/`segments`.
52
+ * Drives cleanup of stale handle buckets when a re-resolved segment
53
+ * pushed nothing. */
54
+ resolvedIds?: string[];
44
55
  historyKey: string;
45
- }
56
+ },
46
57
  ): Promise<void> {
47
- const { eventController, store, matched, isPartial, historyKey } = opts;
58
+ const {
59
+ eventController,
60
+ store,
61
+ matched,
62
+ isPartial,
63
+ resolvedIds,
64
+ historyKey,
65
+ } = opts;
48
66
 
49
67
  let yieldCount = 0;
50
68
  for await (const handleData of handlesGenerator) {
@@ -53,13 +71,13 @@ async function processHandles(
53
71
  // the current route's breadcrumbs (e.g., quick popstate after clicking a link).
54
72
  if (historyKey !== store.getHistoryKey()) {
55
73
  console.log(
56
- "[NavigationProvider] Stopping handle processing - user navigated away"
74
+ "[NavigationProvider] Stopping handle processing - user navigated away",
57
75
  );
58
76
  return;
59
77
  }
60
78
 
61
79
  yieldCount++;
62
- eventController.setHandleData(handleData, matched, isPartial);
80
+ eventController.setHandleData(handleData, matched, isPartial, resolvedIds);
63
81
  }
64
82
 
65
83
  // Check again before final updates
@@ -67,12 +85,11 @@ async function processHandles(
67
85
  return;
68
86
  }
69
87
 
70
- // For partial updates where the generator yielded nothing (cached handlers),
71
- // we still need to update the segment order to clean up stale handle data.
72
- // This happens when navigating away from a route - the handlers for the new
73
- // route might not push any breadcrumbs, but we still need to remove the old ones.
88
+ // For partial updates where the generator yielded nothing (every
89
+ // re-resolved handler pushed nothing), still call setHandleData so the
90
+ // cleanup pass can clear out stale buckets for those segments.
74
91
  if (yieldCount === 0 && matched) {
75
- eventController.setHandleData({}, matched, true);
92
+ eventController.setHandleData({}, matched, true, resolvedIds);
76
93
  }
77
94
 
78
95
  // After handles processing completes, update the cache's handleData.
@@ -100,9 +117,9 @@ export interface NavigationProviderProps {
100
117
  eventController: EventController;
101
118
 
102
119
  /**
103
- * Initial RSC payload from server
120
+ * Initial rendered tree + metadata from server payload
104
121
  */
105
- initialPayload: RscPayload;
122
+ initialPayload: NavigationUpdate;
106
123
 
107
124
  /**
108
125
  * Navigation bridge for handling navigation
@@ -126,6 +143,25 @@ export interface NavigationProviderProps {
126
143
  * When true, keeps TLS alive by sending HEAD requests after idle periods.
127
144
  */
128
145
  warmupEnabled?: boolean;
146
+
147
+ /**
148
+ * App version from server payload.
149
+ * Used only as a fallback when `appShellRef` is not supplied.
150
+ */
151
+ version?: string;
152
+
153
+ /**
154
+ * URL prefix for all routes (from createRouter({ basename })).
155
+ * Used only as a fallback when `appShellRef` is not supplied.
156
+ */
157
+ basename?: string;
158
+
159
+ /**
160
+ * Live app-shell ref. When provided, the context's `basename` and `version`
161
+ * properties become live getters that track app-switch updates without
162
+ * invalidating the memoized context value.
163
+ */
164
+ appShellRef?: AppShellRef;
129
165
  }
130
166
 
131
167
  /**
@@ -157,6 +193,9 @@ export function NavigationProvider({
157
193
  themeConfig,
158
194
  initialTheme,
159
195
  warmupEnabled,
196
+ version,
197
+ basename,
198
+ appShellRef,
160
199
  }: NavigationProviderProps): ReactNode {
161
200
  // Track current payload for rendering (this triggers re-renders)
162
201
  const [payload, setPayload] = useState(initialPayload);
@@ -168,7 +207,7 @@ export function NavigationProvider({
168
207
  async (url: string, options?: NavigateOptions): Promise<void> => {
169
208
  await bridge.navigate(url, options);
170
209
  },
171
- []
210
+ [],
172
211
  );
173
212
 
174
213
  /**
@@ -178,16 +217,39 @@ export function NavigationProvider({
178
217
  await bridge.refresh();
179
218
  }, []);
180
219
 
181
- // Context value is stable (store, eventController, navigate, refresh never change)
182
- const contextValue = useMemo<NavigationStoreContextValue>(
183
- () => ({
220
+ // Context value is stable (store, eventController, navigate, refresh never
221
+ // change). When an appShellRef is supplied, `basename` and `version` are
222
+ // installed as live getters so app-switch transitions (which update the ref)
223
+ // propagate to consumers without forcing a tree-wide rerender.
224
+ const contextValue = useMemo<NavigationStoreContextValue>(() => {
225
+ if (appShellRef) {
226
+ const value = {
227
+ store,
228
+ eventController,
229
+ navigate,
230
+ refresh,
231
+ } as NavigationStoreContextValue;
232
+ Object.defineProperty(value, "basename", {
233
+ configurable: true,
234
+ enumerable: true,
235
+ get: () => appShellRef.get().basename,
236
+ });
237
+ Object.defineProperty(value, "version", {
238
+ configurable: true,
239
+ enumerable: true,
240
+ get: () => appShellRef.get().version,
241
+ });
242
+ return value;
243
+ }
244
+ return {
184
245
  store,
185
246
  eventController,
186
247
  navigate,
187
248
  refresh,
188
- }),
189
- []
190
- );
249
+ version,
250
+ basename,
251
+ };
252
+ }, []);
191
253
 
192
254
  // Connection warmup: keep TLS alive after idle periods.
193
255
  // After 60s of no user interaction, marks connection as "cold".
@@ -252,7 +314,12 @@ export function NavigationProvider({
252
314
  }
253
315
 
254
316
  // Activity events that reset the idle timer
255
- const activityEvents = ["mousemove", "keydown", "touchstart", "scroll"] as const;
317
+ const activityEvents = [
318
+ "mousemove",
319
+ "keydown",
320
+ "touchstart",
321
+ "scroll",
322
+ ] as const;
256
323
  const activityOptions: AddEventListenerOptions = { passive: true };
257
324
 
258
325
  for (const event of activityEvents) {
@@ -271,14 +338,62 @@ export function NavigationProvider({
271
338
  };
272
339
  }, [warmupEnabled]);
273
340
 
341
+ // Cancel non-matching prefetches when navigation starts.
342
+ // Frees connections so the navigation fetch isn't competing with
343
+ // speculative prefetches. The prefetch matching the navigation target
344
+ // is kept alive so it can be reused via consumeInflightPrefetch.
345
+ useEffect(() => {
346
+ let wasIdle = true;
347
+ const unsub = eventController.subscribe(() => {
348
+ const state = eventController.getState();
349
+ const isIdle = state.state === "idle" && !state.isStreaming;
350
+ if (wasIdle && !isIdle) {
351
+ cancelAllPrefetches(state.pendingUrl);
352
+ }
353
+ wasIdle = isIdle;
354
+ });
355
+ return unsub;
356
+ }, [eventController]);
357
+
358
+ // Pending scroll action to apply after React commits
359
+ const pendingScrollRef = useRef<NavigationUpdate["scroll"]>(undefined);
360
+
361
+ // Apply scroll after React commits the new content to the DOM
362
+ useLayoutEffect(() => {
363
+ const scrollAction = pendingScrollRef.current;
364
+ if (!scrollAction) return;
365
+ pendingScrollRef.current = undefined;
366
+
367
+ if (scrollAction.enabled === false) return;
368
+
369
+ handleNavigationEnd({
370
+ restore: scrollAction.restore,
371
+ scroll: scrollAction.enabled,
372
+ isStreaming: scrollAction.isStreaming,
373
+ });
374
+ });
375
+
274
376
  // Subscribe to UI updates (for re-rendering the tree)
275
377
  useEffect(() => {
276
378
  const unsubscribe = store.onUpdate((update) => {
379
+ // Capture scroll intent — it will be applied in useLayoutEffect
380
+ // after React commits this state update to the DOM.
381
+ // Always assign (even undefined) to clear stale scroll from prior navigations,
382
+ // so server actions or error updates don't accidentally replay old scroll.
383
+ pendingScrollRef.current = update.scroll;
384
+
277
385
  setPayload({
278
386
  root: update.root,
279
387
  metadata: update.metadata,
280
388
  });
281
389
 
390
+ // Update route params. Only reset when the server actually sends a params
391
+ // map — an absent `params` field means "no change" (e.g., legacy action
392
+ // responses that omitted params). Explicit `{}` still clears correctly.
393
+ if (update.metadata.params !== undefined) {
394
+ eventController.setParams(update.metadata.params);
395
+ }
396
+
282
397
  // Update handle data progressively as it streams in
283
398
  if (update.metadata.handles) {
284
399
  // Capture historyKey now - by the time async processing completes,
@@ -290,9 +405,10 @@ export function NavigationProvider({
290
405
  store,
291
406
  matched: update.metadata.matched,
292
407
  isPartial: update.metadata.isPartial,
408
+ resolvedIds: update.metadata.resolvedIds,
293
409
  historyKey,
294
410
  }).catch((err) =>
295
- console.error("[NavigationProvider] Error consuming handles:", err)
411
+ console.error("[NavigationProvider] Error consuming handles:", err),
296
412
  );
297
413
  } else if (update.metadata.cachedHandleData) {
298
414
  // For back/forward navigation from cache, restore the cached handleData
@@ -300,14 +416,15 @@ export function NavigationProvider({
300
416
  eventController.setHandleData(
301
417
  update.metadata.cachedHandleData,
302
418
  update.metadata.matched,
303
- false // full replace - restore entire cached state
419
+ false, // full replace - restore entire cached state
304
420
  );
305
421
  } else if (update.metadata.matched) {
306
422
  // For cached navigations without handleData, update segmentOrder to clean up stale data
307
423
  eventController.setHandleData(
308
424
  {}, // Empty data - all existing data not in matched will be cleaned up
309
425
  update.metadata.matched,
310
- true // partial update - will clean up segments not in matched
426
+ true, // partial update - will clean up segments not in matched
427
+ update.metadata.resolvedIds,
311
428
  );
312
429
  }
313
430
  });
@@ -329,7 +446,11 @@ export function NavigationProvider({
329
446
  // Build the content tree
330
447
  let content = <RootErrorBoundary>{root}</RootErrorBoundary>;
331
448
 
332
- // Wrap with ThemeProvider when theme is enabled
449
+ // Wrap with ThemeProvider when theme is enabled. The ThemeProvider is
450
+ // document-lifetime: its config comes from the initial load and does NOT
451
+ // swap on cross-app transitions, because the ThemeProvider sits above the
452
+ // segment tree and a smooth (no-reload) app switch cannot safely remount
453
+ // it. A new theme config only takes effect on a full document load.
333
454
  if (themeConfig) {
334
455
  content = (
335
456
  <ThemeProvider config={themeConfig} initialTheme={initialTheme}>
@@ -338,6 +459,13 @@ export function NavigationProvider({
338
459
  );
339
460
  }
340
461
 
462
+ // Match SSR tree shape: NonceContext.Provider is always present so
463
+ // hydration sees the same component tree. Value is undefined on the
464
+ // client — CSP nonces are a server-side HTML concern.
465
+ content = (
466
+ <NonceContext.Provider value={undefined}>{content}</NonceContext.Provider>
467
+ );
468
+
341
469
  return (
342
470
  <NavigationStoreContext.Provider value={contextValue}>
343
471
  {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,55 @@
1
+ /**
2
+ * Build the handle-collection segment order from a raw `matched` list.
3
+ *
4
+ * Two responsibilities:
5
+ *
6
+ * 1. Drop loader sub-ids ("D" followed by a digit, e.g. "M0L0D1.user") —
7
+ * loaders never push handles.
8
+ *
9
+ * 2. Place each parallel slot id (contains ".@") immediately after its
10
+ * parent layout/route id. Raw segment-resolution emission order does NOT
11
+ * guarantee this: route-mounted parallels are resolved/pushed BEFORE the
12
+ * route handler's segment is appended (see fresh.ts:resolveSegment for
13
+ * routes, and revalidation.ts ~915-919), so matched can read
14
+ * `[..., R0.@panel, R0]`. collectHandleData consumes segmentOrder verbatim
15
+ * with later-wins semantics, so without normalization the route handler's
16
+ * Meta would override the slot's more-specific Meta — backwards.
17
+ *
18
+ * Slot-id format is `<parentShortCode>.@<slotName>`; `parentShortCode` never
19
+ * contains ".@", so splitting at the first ".@" reliably yields the parent.
20
+ */
21
+ export function filterSegmentOrder(matched: string[]): string[] {
22
+ const slotsByParent = new Map<string, string[]>();
23
+ const nonSlots: string[] = [];
24
+ const nonSlotSet = new Set<string>();
25
+
26
+ for (const id of matched) {
27
+ if (/D\d+\./.test(id)) continue;
28
+ const slotIdx = id.indexOf(".@");
29
+ if (slotIdx >= 0) {
30
+ const parent = id.slice(0, slotIdx);
31
+ const list = slotsByParent.get(parent);
32
+ if (list) {
33
+ list.push(id);
34
+ } else {
35
+ slotsByParent.set(parent, [id]);
36
+ }
37
+ } else {
38
+ nonSlots.push(id);
39
+ nonSlotSet.add(id);
40
+ }
41
+ }
42
+
43
+ const result: string[] = [];
44
+ for (const id of nonSlots) {
45
+ result.push(id);
46
+ const slots = slotsByParent.get(id);
47
+ if (slots) result.push(...slots);
48
+ }
49
+ // Defensive: any slot whose parent is missing from the filtered list still
50
+ // gets included rather than silently dropped. Shouldn't happen in practice.
51
+ for (const [parent, slots] of slotsByParent) {
52
+ if (!nonSlotSet.has(parent)) result.push(...slots);
53
+ }
54
+ return result;
55
+ }
@@ -1,20 +1,27 @@
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";
22
+
23
+ // Mount-aware reverse hook
24
+ export { useReverse } from "./use-reverse.js";
18
25
 
19
26
  // Client cache controls hook
20
27
  export {
@@ -35,11 +42,7 @@ export {
35
42
  } from "./context.js";
36
43
 
37
44
  // Link component
38
- export {
39
- Link,
40
- type LinkProps,
41
- type PrefetchStrategy,
42
- } from "./Link.js";
45
+ export { Link, type LinkProps, type PrefetchStrategy } from "./Link.js";
43
46
 
44
47
  // Link status hook
45
48
  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
  }