@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
@@ -1,67 +1,71 @@
1
1
  "use client";
2
2
 
3
- import React, { forwardRef, useCallback, useContext, useRef, type ForwardRefExoticComponent, type RefAttributes } from "react";
3
+ import React, {
4
+ forwardRef,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ type ForwardRefExoticComponent,
11
+ type RefAttributes,
12
+ } from "react";
4
13
  import { NavigationStoreContext } from "./context.js";
5
14
  import { LinkContext } from "./use-link-status.js";
6
15
  import type { NavigateOptions } from "../types.js";
16
+ import { isHashOnlyNavigation } from "../link-interceptor.js";
7
17
  import {
8
- type LocationStateEntry,
9
18
  isLocationStateEntry,
19
+ type LocationStateEntry,
10
20
  resolveLocationStateEntries,
11
21
  } from "./location-state.js";
12
22
 
13
23
  /**
14
- * State value or getter function for just-in-time state resolution (legacy)
24
+ * State prop type for Link component.
25
+ * - LocationStateEntry[]: Type-safe state entries via createLocationState()
26
+ * - StateOrGetter: Plain state object or click-time getter function
27
+ * - Record<string, unknown>: Plain state object passed to history.pushState
15
28
  */
16
29
  export type StateOrGetter<T = unknown> = T | (() => T);
17
30
 
18
- /**
19
- * State prop type for Link component
20
- * - LocationStateEntry[]: Type-safe state entries (always lazy)
21
- * - StateOrGetter: Legacy format for backwards compatibility
22
- */
23
- export type LinkState = LocationStateEntry[] | StateOrGetter;
31
+ export type LinkState =
32
+ | LocationStateEntry[]
33
+ | StateOrGetter<Record<string, unknown>>;
24
34
 
25
- // Track prefetched URLs to avoid duplicate <link> elements
26
- const prefetchedUrls = new Set<string>();
27
-
28
- /**
29
- * Inject a <link rel="prefetch"> element into the document head
30
- * for the given URL with RSC partial request parameters.
31
- */
32
- function prefetchUrl(url: string, segmentIds: string[]): void {
33
- if (prefetchedUrls.has(url)) return;
34
- prefetchedUrls.add(url);
35
-
36
- // Build RSC partial URL with segment IDs
37
- const targetUrl = new URL(url, window.location.origin);
38
- targetUrl.searchParams.set("_rsc_partial", "true");
39
- if (segmentIds.length > 0) {
40
- targetUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
41
- }
35
+ import { prefetchDirect, prefetchQueued } from "../prefetch/fetch.js";
36
+ import { getAppVersion } from "../app-version.js";
37
+ import {
38
+ observeForPrefetch,
39
+ unobserveForPrefetch,
40
+ } from "../prefetch/observer.js";
42
41
 
43
- // Inject <link rel="prefetch"> into head
44
- const link = document.createElement("link");
45
- link.rel = "prefetch";
46
- link.href = targetUrl.toString();
47
- link.as = "fetch";
48
- document.head.appendChild(link);
49
- }
42
+ // Touch device detection for adaptive strategy.
43
+ // Checked once at module load (Link.tsx is "use client", runs only in browser).
44
+ const isTouchDevice =
45
+ typeof window !== "undefined" && window.matchMedia("(hover: none)").matches;
50
46
 
51
47
  /**
52
48
  * Prefetch strategy for the Link component
53
- * - "hover": Prefetch on mouse enter (uses native <link rel="prefetch">)
54
- * - "viewport": Prefetch when link enters viewport (not yet implemented)
55
- * - "hybrid": Hover on desktop, viewport on mobile (not yet implemented)
49
+ * - "hover": Prefetch on mouse enter (direct, no queue)
50
+ * - "viewport": Prefetch when link enters viewport (queued, waits for idle)
51
+ * - "render": Prefetch on component mount regardless of visibility (queued, waits for idle)
52
+ * - "adaptive": Hover on pointer devices, viewport on touch devices
56
53
  * - "none": No prefetching (default)
57
54
  */
58
- export type PrefetchStrategy = "hover" | "viewport" | "hybrid" | "none";
55
+ export type PrefetchStrategy =
56
+ | "hover"
57
+ | "viewport"
58
+ | "render"
59
+ | "adaptive"
60
+ | "none";
59
61
 
60
62
  /**
61
63
  * Link component props
62
64
  */
63
- export interface LinkProps
64
- extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
65
+ export interface LinkProps extends Omit<
66
+ React.AnchorHTMLAttributes<HTMLAnchorElement>,
67
+ "href"
68
+ > {
65
69
  /**
66
70
  * The URL to navigate to (typically from router.reverse())
67
71
  */
@@ -78,11 +82,46 @@ export interface LinkProps
78
82
  * Force full document navigation instead of SPA
79
83
  */
80
84
  reloadDocument?: boolean;
85
+ /**
86
+ * Whether to revalidate server data on navigation.
87
+ * Set to `false` to skip the RSC server fetch and only update the URL.
88
+ *
89
+ * Only takes effect when the pathname stays the same (search param / hash changes).
90
+ * If the pathname changes, this option is ignored and a full navigation occurs.
91
+ *
92
+ * @default true
93
+ */
94
+ revalidate?: boolean;
81
95
  /**
82
96
  * Prefetch strategy for the link destination
83
97
  * @default "none"
84
98
  */
85
99
  prefetch?: PrefetchStrategy;
100
+ /**
101
+ * Opt-in override for the prefetch cache scope.
102
+ *
103
+ * The default cache is source-agnostic: one shared entry per target,
104
+ * keyed on Rango state + target URL. This is correct for routes whose
105
+ * response shape doesn't depend on where the user navigates from.
106
+ *
107
+ * Set `":source"` when this Link's response would legitimately differ
108
+ * based on the source page — typically when the target route (or one
109
+ * of its layouts) uses a custom `revalidate()` handler that reads
110
+ * `currentUrl` / `currentParams`, and the wildcard entry would
111
+ * therefore serve the wrong diff to a navigation from a different
112
+ * source.
113
+ *
114
+ * Intercept responses are auto-scoped to the source via a server-side
115
+ * tag, so `":source"` is only needed for custom revalidation logic.
116
+ *
117
+ * @example
118
+ * ```tsx
119
+ * // Route uses a `revalidate()` that branches on currentUrl — opt in
120
+ * // so prefetches don't bleed across source pages.
121
+ * <Link to="/dashboard" prefetch="hover" prefetchKey=":source" />
122
+ * ```
123
+ */
124
+ prefetchKey?: ":source";
86
125
  /**
87
126
  * State to pass to history.pushState/replaceState.
88
127
  * Accessible via useLocationState() hook.
@@ -90,16 +129,29 @@ export interface LinkProps
90
129
  * @example
91
130
  * ```tsx
92
131
  * // Type-safe state with createLocationState (recommended)
93
- * const ProductState = createLocationState((p: Product) => ({ name: p.name }));
94
- * <Link to="/product" state={[ProductState(product)]}>View</Link>
132
+ * const ProductState = createLocationState<{ name: string; price: number }>();
133
+ * <Link to="/product" state={[ProductState({ name: product.name, price: product.price })]}>
134
+ * View
135
+ * </Link>
136
+ *
137
+ * // Type-safe just-in-time state (getter called at click time, not render time).
138
+ * // Must be in a client component -- getter can't cross the RSC boundary.
139
+ * <Link
140
+ * to="/product"
141
+ * state={[ProductState(() => ({ name: product.name, price: product.price }))]}
142
+ * >
143
+ * View
144
+ * </Link>
95
145
  *
96
146
  * // Multiple typed states
97
- * <Link to="/checkout" state={[ProductState(p), CartState(c)]}>Checkout</Link>
147
+ * <Link to="/checkout" state={[ProductState({ name: p.name, price: p.price }), CartState(c)]}>
148
+ * Checkout
149
+ * </Link>
98
150
  *
99
- * // Legacy: static state
151
+ * // Plain static state
100
152
  * <Link to="/product" state={{ from: "list" }}>View</Link>
101
153
  *
102
- * // Legacy: dynamic state (called at click time)
154
+ * // Plain just-in-time state (called at click time, requires client component)
103
155
  * <Link to="/product" state={() => ({ scrollY: window.scrollY })}>View</Link>
104
156
  * ```
105
157
  */
@@ -147,23 +199,56 @@ function isExternalUrl(href: string): boolean {
147
199
  * <Link to="https://example.com">External</Link>
148
200
  * ```
149
201
  */
150
- export const Link: ForwardRefExoticComponent<LinkProps & RefAttributes<HTMLAnchorElement>> = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
202
+ export const Link: ForwardRefExoticComponent<
203
+ LinkProps & RefAttributes<HTMLAnchorElement>
204
+ > = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
151
205
  {
152
206
  to,
153
207
  replace = false,
154
208
  scroll = true,
155
209
  reloadDocument = false,
210
+ revalidate,
156
211
  prefetch = "none",
212
+ prefetchKey,
157
213
  state,
158
214
  children,
159
215
  onClick,
160
216
  ...props
161
217
  },
162
- ref
218
+ ref,
163
219
  ) {
164
220
  const ctx = useContext(NavigationStoreContext);
165
221
  const isExternal = isExternalUrl(to);
166
222
 
223
+ // Auto-prefix with basename for app-local paths.
224
+ // Skip if external, already prefixed, or not a root-relative path.
225
+ const resolvedTo = useMemo(() => {
226
+ if (isExternal) return to;
227
+ const bn = ctx?.basename;
228
+ if (!bn || !to.startsWith("/") || to.startsWith(bn + "/") || to === bn)
229
+ return to;
230
+ return to === "/" ? bn : bn + to;
231
+ }, [to, isExternal, ctx?.basename]);
232
+
233
+ // Resolve adaptive: viewport on touch devices, hover on pointer devices
234
+ const resolvedStrategy =
235
+ prefetch === "adaptive" ? (isTouchDevice ? "viewport" : "hover") : prefetch;
236
+
237
+ // Internal ref for viewport observation; merge with forwarded ref
238
+ const internalRef = useRef<HTMLAnchorElement | null>(null);
239
+ const setRef = useCallback(
240
+ (node: HTMLAnchorElement | null) => {
241
+ internalRef.current = node;
242
+ if (typeof ref === "function") {
243
+ ref(node);
244
+ } else if (ref) {
245
+ (ref as React.MutableRefObject<HTMLAnchorElement | null>).current =
246
+ node;
247
+ }
248
+ },
249
+ [ref],
250
+ );
251
+
167
252
  // Use ref to always get the latest state/getter without adding to useCallback deps
168
253
  // This enables just-in-time state resolution without causing re-renders
169
254
  const stateRef = useRef(state);
@@ -194,55 +279,154 @@ export const Link: ForwardRefExoticComponent<LinkProps & RefAttributes<HTMLAncho
194
279
  const target = (e.currentTarget as HTMLAnchorElement).target;
195
280
  if (target && target !== "_self") return;
196
281
 
282
+ // Hash-only navigation: let the browser handle anchor scrolling natively.
283
+ if (isHashOnlyNavigation(e.currentTarget as HTMLAnchorElement)) {
284
+ return;
285
+ }
286
+
287
+ // No navigation context (outside provider): fall back to native navigation.
288
+ if (!ctx?.navigate) {
289
+ return;
290
+ }
291
+
197
292
  // Prevent default and use SPA navigation
198
293
  e.preventDefault();
199
294
  // Stop propagation to prevent link-interceptor from also handling this
200
295
  e.stopPropagation();
201
296
 
202
- if (ctx?.navigate) {
203
- // Resolve state just-in-time based on format
204
- let resolvedState: unknown;
205
- const currentState = stateRef.current;
206
-
207
- if (Array.isArray(currentState) && currentState.length > 0 && isLocationStateEntry(currentState[0])) {
208
- // Type-safe LocationStateEntry[] - resolve each entry into keyed object
209
- resolvedState = resolveLocationStateEntries(currentState as LocationStateEntry[]);
210
- } else if (typeof currentState === "function") {
211
- // Legacy getter function
212
- resolvedState = currentState();
213
- } else {
214
- // Legacy static value
215
- resolvedState = currentState;
216
- }
297
+ const currentState = stateRef.current;
298
+ let resolvedState: unknown;
217
299
 
218
- ctx.navigate(to, { replace, scroll, state: resolvedState });
300
+ if (
301
+ Array.isArray(currentState) &&
302
+ currentState.length > 0 &&
303
+ isLocationStateEntry(currentState[0])
304
+ ) {
305
+ resolvedState = resolveLocationStateEntries(
306
+ currentState as LocationStateEntry[],
307
+ );
308
+ } else if (typeof currentState === "function") {
309
+ resolvedState = currentState();
310
+ } else if (currentState != null) {
311
+ resolvedState = currentState;
219
312
  }
313
+
314
+ ctx.navigate(resolvedTo, {
315
+ replace,
316
+ scroll,
317
+ state: resolvedState,
318
+ revalidate,
319
+ });
220
320
  },
221
- [to, isExternal, reloadDocument, replace, scroll, ctx, onClick]
321
+ [
322
+ resolvedTo,
323
+ isExternal,
324
+ reloadDocument,
325
+ replace,
326
+ scroll,
327
+ revalidate,
328
+ ctx,
329
+ onClick,
330
+ ],
222
331
  );
223
332
 
224
333
  const handleMouseEnter = useCallback(() => {
225
- if (prefetch === "hover" && !isExternal && ctx?.store) {
334
+ if (
335
+ (resolvedStrategy === "hover" || resolvedStrategy === "viewport") &&
336
+ !isExternal &&
337
+ ctx?.store
338
+ ) {
339
+ // For "hover", this is the primary prefetch trigger.
340
+ // For "viewport", this upgrades/prioritizes a potentially queued
341
+ // prefetch — prefetchDirect bypasses the queue, and hasPrefetch
342
+ // deduplicates if the viewport prefetch already completed.
226
343
  const segmentState = ctx.store.getSegmentState();
227
- prefetchUrl(to, segmentState.currentSegmentIds);
344
+ prefetchDirect(
345
+ resolvedTo,
346
+ segmentState.currentSegmentIds,
347
+ getAppVersion(),
348
+ ctx.store.getRouterId?.(),
349
+ prefetchKey,
350
+ );
228
351
  }
229
- }, [prefetch, to, isExternal, ctx]);
352
+ }, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
353
+
354
+ // Viewport/render prefetch: waits for idle before starting,
355
+ // uses concurrency-limited queue to avoid flooding.
356
+ useEffect(() => {
357
+ if (isExternal || !ctx?.store) return;
358
+ const isViewport = resolvedStrategy === "viewport";
359
+ const isRender = resolvedStrategy === "render";
360
+ if (!isViewport && !isRender) return;
361
+
362
+ let cancelled = false;
363
+ let unsubIdle: (() => void) | undefined;
364
+ let observedElement: Element | null = null;
365
+
366
+ const triggerPrefetch = () => {
367
+ if (cancelled) return;
368
+ const segmentState = ctx.store.getSegmentState();
369
+ prefetchQueued(
370
+ resolvedTo,
371
+ segmentState.currentSegmentIds,
372
+ getAppVersion(),
373
+ ctx.store.getRouterId?.(),
374
+ prefetchKey,
375
+ );
376
+ };
377
+
378
+ // Schedule prefetch only when the app is idle (no navigation/streaming).
379
+ // This avoids competing with hydration and active navigation fetches.
380
+ const scheduleWhenIdle = (callback: () => void) => {
381
+ const state = ctx.eventController.getState();
382
+ if (state.state === "idle" && !state.isStreaming) {
383
+ callback();
384
+ return;
385
+ }
386
+ const unsub = ctx.eventController.subscribe(() => {
387
+ const s = ctx.eventController.getState();
388
+ if (s.state === "idle" && !s.isStreaming) {
389
+ unsub();
390
+ callback();
391
+ }
392
+ });
393
+ unsubIdle = unsub;
394
+ };
395
+
396
+ if (isRender) {
397
+ scheduleWhenIdle(triggerPrefetch);
398
+ } else if (isViewport) {
399
+ const element = internalRef.current;
400
+ if (!element) return;
401
+ observedElement = element;
402
+ observeForPrefetch(element, () => {
403
+ scheduleWhenIdle(triggerPrefetch);
404
+ });
405
+ }
406
+
407
+ return () => {
408
+ cancelled = true;
409
+ unsubIdle?.();
410
+ if (isViewport && observedElement) {
411
+ unobserveForPrefetch(observedElement);
412
+ }
413
+ };
414
+ }, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
230
415
 
231
416
  return (
232
417
  <a
233
- ref={ref}
234
- href={to}
418
+ ref={setRef}
419
+ href={resolvedTo}
235
420
  onClick={handleClick}
236
421
  onMouseEnter={handleMouseEnter}
237
422
  data-link-component
238
423
  data-external={isExternal ? "" : undefined}
239
424
  data-scroll={scroll === false ? "false" : undefined}
240
425
  data-replace={replace ? "true" : undefined}
426
+ data-revalidate={revalidate === false ? "false" : undefined}
241
427
  {...props}
242
428
  >
243
- <LinkContext.Provider value={to}>
244
- {children}
245
- </LinkContext.Provider>
429
+ <LinkContext.Provider value={resolvedTo}>{children}</LinkContext.Provider>
246
430
  </a>
247
431
  );
248
432
  });