@rangojs/router 0.0.0-experimental.0f44aca1

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 (305) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +5214 -0
  5. package/package.json +176 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +220 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +645 -0
  43. package/src/browser/navigation-client.ts +215 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +295 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +550 -0
  48. package/src/browser/prefetch/cache.ts +146 -0
  49. package/src/browser/prefetch/fetch.ts +135 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +42 -0
  52. package/src/browser/prefetch/queue.ts +88 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +360 -0
  55. package/src/browser/react/NavigationProvider.tsx +386 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +431 -0
  79. package/src/browser/scroll-restoration.ts +400 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +538 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +469 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +540 -0
  105. package/src/cache/cf/index.ts +25 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +43 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +275 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +158 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +395 -0
  172. package/src/router/lazy-includes.ts +234 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +248 -0
  175. package/src/router/manifest.ts +267 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +192 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +748 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +316 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1239 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +289 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1002 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +235 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +914 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +102 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +110 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +131 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +365 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -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 +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +254 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +510 -0
  298. package/src/vite/router-discovery.ts +785 -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/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,360 @@
1
+ "use client";
2
+
3
+ import React, {
4
+ forwardRef,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useRef,
9
+ type ForwardRefExoticComponent,
10
+ type RefAttributes,
11
+ } from "react";
12
+ import { NavigationStoreContext } from "./context.js";
13
+ import { LinkContext } from "./use-link-status.js";
14
+ import type { NavigateOptions } from "../types.js";
15
+ import { isHashOnlyNavigation } from "../link-interceptor.js";
16
+ import {
17
+ isLocationStateEntry,
18
+ type LocationStateEntry,
19
+ resolveLocationStateEntries,
20
+ } from "./location-state.js";
21
+
22
+ /**
23
+ * State prop type for Link component.
24
+ * - LocationStateEntry[]: Type-safe state entries via createLocationState()
25
+ * - StateOrGetter: Plain state object or click-time getter function
26
+ * - Record<string, unknown>: Plain state object passed to history.pushState
27
+ */
28
+ export type StateOrGetter<T = unknown> = T | (() => T);
29
+
30
+ export type LinkState =
31
+ | LocationStateEntry[]
32
+ | StateOrGetter<Record<string, unknown>>;
33
+
34
+ import { prefetchDirect, prefetchQueued } from "../prefetch/fetch.js";
35
+ import {
36
+ observeForPrefetch,
37
+ unobserveForPrefetch,
38
+ } from "../prefetch/observer.js";
39
+
40
+ // Touch device detection for adaptive strategy.
41
+ // Checked once at module load (Link.tsx is "use client", runs only in browser).
42
+ const isTouchDevice =
43
+ typeof window !== "undefined" && window.matchMedia("(hover: none)").matches;
44
+
45
+ /**
46
+ * Prefetch strategy for the Link component
47
+ * - "hover": Prefetch on mouse enter (direct, no queue)
48
+ * - "viewport": Prefetch when link enters viewport (queued, waits for idle)
49
+ * - "render": Prefetch on component mount regardless of visibility (queued, waits for idle)
50
+ * - "adaptive": Hover on pointer devices, viewport on touch devices
51
+ * - "none": No prefetching (default)
52
+ */
53
+ export type PrefetchStrategy =
54
+ | "hover"
55
+ | "viewport"
56
+ | "render"
57
+ | "adaptive"
58
+ | "none";
59
+
60
+ /**
61
+ * Link component props
62
+ */
63
+ export interface LinkProps extends Omit<
64
+ React.AnchorHTMLAttributes<HTMLAnchorElement>,
65
+ "href"
66
+ > {
67
+ /**
68
+ * The URL to navigate to (typically from router.reverse())
69
+ */
70
+ to: string;
71
+ /**
72
+ * Replace current history entry instead of pushing
73
+ */
74
+ replace?: boolean;
75
+ /**
76
+ * Scroll to top after navigation (default: true)
77
+ */
78
+ scroll?: boolean;
79
+ /**
80
+ * Force full document navigation instead of SPA
81
+ */
82
+ reloadDocument?: boolean;
83
+ /**
84
+ * Whether to revalidate server data on navigation.
85
+ * Set to `false` to skip the RSC server fetch and only update the URL.
86
+ *
87
+ * Only takes effect when the pathname stays the same (search param / hash changes).
88
+ * If the pathname changes, this option is ignored and a full navigation occurs.
89
+ *
90
+ * @default true
91
+ */
92
+ revalidate?: boolean;
93
+ /**
94
+ * Prefetch strategy for the link destination
95
+ * @default "none"
96
+ */
97
+ prefetch?: PrefetchStrategy;
98
+ /**
99
+ * State to pass to history.pushState/replaceState.
100
+ * Accessible via useLocationState() hook.
101
+ *
102
+ * @example
103
+ * ```tsx
104
+ * // Type-safe state with createLocationState (recommended)
105
+ * const ProductState = createLocationState<{ name: string; price: number }>();
106
+ * <Link to="/product" state={[ProductState({ name: product.name, price: product.price })]}>
107
+ * View
108
+ * </Link>
109
+ *
110
+ * // Type-safe just-in-time state (getter called at click time, not render time).
111
+ * // Must be in a client component -- getter can't cross the RSC boundary.
112
+ * <Link
113
+ * to="/product"
114
+ * state={[ProductState(() => ({ name: product.name, price: product.price }))]}
115
+ * >
116
+ * View
117
+ * </Link>
118
+ *
119
+ * // Multiple typed states
120
+ * <Link to="/checkout" state={[ProductState({ name: p.name, price: p.price }), CartState(c)]}>
121
+ * Checkout
122
+ * </Link>
123
+ *
124
+ * // Plain static state
125
+ * <Link to="/product" state={{ from: "list" }}>View</Link>
126
+ *
127
+ * // Plain just-in-time state (called at click time, requires client component)
128
+ * <Link to="/product" state={() => ({ scrollY: window.scrollY })}>View</Link>
129
+ * ```
130
+ */
131
+ state?: LinkState;
132
+ children: React.ReactNode;
133
+ }
134
+
135
+ /**
136
+ * Check if URL is external (different origin)
137
+ */
138
+ function isExternalUrl(href: string): boolean {
139
+ // Protocol-relative URLs
140
+ if (href.startsWith("//")) return true;
141
+
142
+ // Absolute URLs
143
+ if (href.startsWith("http://") || href.startsWith("https://")) {
144
+ try {
145
+ return new URL(href).origin !== window.location.origin;
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
151
+ // Special protocols (mailto, tel, etc.)
152
+ if (/^[a-z][a-z0-9+.-]*:/i.test(href)) {
153
+ return true;
154
+ }
155
+
156
+ return false;
157
+ }
158
+
159
+ /**
160
+ * Type-safe Link component for SPA navigation
161
+ *
162
+ * Works with router.reverse() for type-safe URLs:
163
+ * ```tsx
164
+ * <Link to={router.reverse("shop.products.detail", { slug: "my-product" })}>
165
+ * View Product
166
+ * </Link>
167
+ * ```
168
+ *
169
+ * Also supports regular URLs:
170
+ * ```tsx
171
+ * <Link to="/about">About</Link>
172
+ * <Link to="https://example.com">External</Link>
173
+ * ```
174
+ */
175
+ export const Link: ForwardRefExoticComponent<
176
+ LinkProps & RefAttributes<HTMLAnchorElement>
177
+ > = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
178
+ {
179
+ to,
180
+ replace = false,
181
+ scroll = true,
182
+ reloadDocument = false,
183
+ revalidate,
184
+ prefetch = "none",
185
+ state,
186
+ children,
187
+ onClick,
188
+ ...props
189
+ },
190
+ ref,
191
+ ) {
192
+ const ctx = useContext(NavigationStoreContext);
193
+ const isExternal = isExternalUrl(to);
194
+
195
+ // Resolve adaptive: viewport on touch devices, hover on pointer devices
196
+ const resolvedStrategy =
197
+ prefetch === "adaptive" ? (isTouchDevice ? "viewport" : "hover") : prefetch;
198
+
199
+ // Internal ref for viewport observation; merge with forwarded ref
200
+ const internalRef = useRef<HTMLAnchorElement | null>(null);
201
+ const setRef = useCallback(
202
+ (node: HTMLAnchorElement | null) => {
203
+ internalRef.current = node;
204
+ if (typeof ref === "function") {
205
+ ref(node);
206
+ } else if (ref) {
207
+ (ref as React.MutableRefObject<HTMLAnchorElement | null>).current =
208
+ node;
209
+ }
210
+ },
211
+ [ref],
212
+ );
213
+
214
+ // Use ref to always get the latest state/getter without adding to useCallback deps
215
+ // This enables just-in-time state resolution without causing re-renders
216
+ const stateRef = useRef(state);
217
+ stateRef.current = state;
218
+
219
+ const handleClick = useCallback(
220
+ (e: React.MouseEvent<HTMLAnchorElement>) => {
221
+ // Call user's onClick handler first
222
+ onClick?.(e);
223
+
224
+ // If user prevented default, respect that
225
+ if (e.defaultPrevented) return;
226
+
227
+ // External links - let browser handle normally
228
+ if (isExternal) return;
229
+
230
+ // Force document navigation if requested
231
+ if (reloadDocument) return;
232
+
233
+ // Allow modifier keys for opening in new tab/window
234
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
235
+
236
+ // Check for download attribute
237
+ if ((e.currentTarget as HTMLAnchorElement).hasAttribute("download"))
238
+ return;
239
+
240
+ // Check for target attribute
241
+ const target = (e.currentTarget as HTMLAnchorElement).target;
242
+ if (target && target !== "_self") return;
243
+
244
+ // Hash-only navigation: let the browser handle anchor scrolling natively.
245
+ if (isHashOnlyNavigation(e.currentTarget as HTMLAnchorElement)) {
246
+ return;
247
+ }
248
+
249
+ // No navigation context (outside provider): fall back to native navigation.
250
+ if (!ctx?.navigate) {
251
+ return;
252
+ }
253
+
254
+ // Prevent default and use SPA navigation
255
+ e.preventDefault();
256
+ // Stop propagation to prevent link-interceptor from also handling this
257
+ e.stopPropagation();
258
+
259
+ const currentState = stateRef.current;
260
+ let resolvedState: unknown;
261
+
262
+ if (
263
+ Array.isArray(currentState) &&
264
+ currentState.length > 0 &&
265
+ isLocationStateEntry(currentState[0])
266
+ ) {
267
+ resolvedState = resolveLocationStateEntries(
268
+ currentState as LocationStateEntry[],
269
+ );
270
+ } else if (typeof currentState === "function") {
271
+ resolvedState = currentState();
272
+ } else if (currentState != null) {
273
+ resolvedState = currentState;
274
+ }
275
+
276
+ ctx.navigate(to, { replace, scroll, state: resolvedState, revalidate });
277
+ },
278
+ [to, isExternal, reloadDocument, replace, scroll, revalidate, ctx, onClick],
279
+ );
280
+
281
+ const handleMouseEnter = useCallback(() => {
282
+ if (resolvedStrategy === "hover" && !isExternal && ctx?.store) {
283
+ const segmentState = ctx.store.getSegmentState();
284
+ prefetchDirect(to, segmentState.currentSegmentIds, ctx.version);
285
+ }
286
+ }, [resolvedStrategy, to, isExternal, ctx]);
287
+
288
+ // Viewport/render prefetch: waits for idle before starting,
289
+ // uses concurrency-limited queue to avoid flooding.
290
+ useEffect(() => {
291
+ if (isExternal || !ctx?.store) return;
292
+ const isViewport = resolvedStrategy === "viewport";
293
+ const isRender = resolvedStrategy === "render";
294
+ if (!isViewport && !isRender) return;
295
+
296
+ let cancelled = false;
297
+ let unsubIdle: (() => void) | undefined;
298
+ let observedElement: Element | null = null;
299
+
300
+ const triggerPrefetch = () => {
301
+ if (cancelled) return;
302
+ const segmentState = ctx.store.getSegmentState();
303
+ prefetchQueued(to, segmentState.currentSegmentIds, ctx.version);
304
+ };
305
+
306
+ // Schedule prefetch only when the app is idle (no navigation/streaming).
307
+ // This avoids competing with hydration and active navigation fetches.
308
+ const scheduleWhenIdle = (callback: () => void) => {
309
+ const state = ctx.eventController.getState();
310
+ if (state.state === "idle" && !state.isStreaming) {
311
+ callback();
312
+ return;
313
+ }
314
+ const unsub = ctx.eventController.subscribe(() => {
315
+ const s = ctx.eventController.getState();
316
+ if (s.state === "idle" && !s.isStreaming) {
317
+ unsub();
318
+ callback();
319
+ }
320
+ });
321
+ unsubIdle = unsub;
322
+ };
323
+
324
+ if (isRender) {
325
+ scheduleWhenIdle(triggerPrefetch);
326
+ } else if (isViewport) {
327
+ const element = internalRef.current;
328
+ if (!element) return;
329
+ observedElement = element;
330
+ observeForPrefetch(element, () => {
331
+ scheduleWhenIdle(triggerPrefetch);
332
+ });
333
+ }
334
+
335
+ return () => {
336
+ cancelled = true;
337
+ unsubIdle?.();
338
+ if (isViewport && observedElement) {
339
+ unobserveForPrefetch(observedElement);
340
+ }
341
+ };
342
+ }, [resolvedStrategy, to, isExternal, ctx]);
343
+
344
+ return (
345
+ <a
346
+ ref={setRef}
347
+ href={to}
348
+ onClick={handleClick}
349
+ onMouseEnter={handleMouseEnter}
350
+ data-link-component
351
+ data-external={isExternal ? "" : undefined}
352
+ data-scroll={scroll === false ? "false" : undefined}
353
+ data-replace={replace ? "true" : undefined}
354
+ data-revalidate={revalidate === false ? "false" : undefined}
355
+ {...props}
356
+ >
357
+ <LinkContext.Provider value={to}>{children}</LinkContext.Provider>
358
+ </a>
359
+ );
360
+ });