@rangojs/router 0.0.0-experimental.002d056c
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.
- package/AGENTS.md +9 -0
- package/README.md +899 -0
- package/dist/bin/rango.js +1606 -0
- package/dist/vite/index.js +5153 -0
- package/package.json +177 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +253 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +638 -0
- package/src/browser/navigation-client.ts +261 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +582 -0
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +145 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +128 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +368 -0
- package/src/browser/react/NavigationProvider.tsx +413 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +464 -0
- package/src/browser/scroll-restoration.ts +397 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +547 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +479 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +382 -0
- package/src/cache/cf/cf-cache-store.ts +982 -0
- package/src/cache/cf/index.ts +29 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +44 -0
- package/src/cache/memory-segment-store.ts +328 -0
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +281 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +397 -0
- package/src/router/lazy-includes.ts +236 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +269 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +193 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +749 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +320 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1242 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +170 -0
- package/src/router.ts +1006 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +237 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +920 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +109 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +108 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +48 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/plugins/expose-action-id.ts +363 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +445 -0
- package/src/vite/router-discovery.ts +777 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,368 @@
|
|
|
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 (
|
|
283
|
+
(resolvedStrategy === "hover" || resolvedStrategy === "viewport") &&
|
|
284
|
+
!isExternal &&
|
|
285
|
+
ctx?.store
|
|
286
|
+
) {
|
|
287
|
+
// For "hover", this is the primary prefetch trigger.
|
|
288
|
+
// For "viewport", this upgrades/prioritizes a potentially queued
|
|
289
|
+
// prefetch — prefetchDirect bypasses the queue, and hasPrefetch
|
|
290
|
+
// deduplicates if the viewport prefetch already completed.
|
|
291
|
+
const segmentState = ctx.store.getSegmentState();
|
|
292
|
+
prefetchDirect(to, segmentState.currentSegmentIds, ctx.version);
|
|
293
|
+
}
|
|
294
|
+
}, [resolvedStrategy, to, isExternal, ctx]);
|
|
295
|
+
|
|
296
|
+
// Viewport/render prefetch: waits for idle before starting,
|
|
297
|
+
// uses concurrency-limited queue to avoid flooding.
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
if (isExternal || !ctx?.store) return;
|
|
300
|
+
const isViewport = resolvedStrategy === "viewport";
|
|
301
|
+
const isRender = resolvedStrategy === "render";
|
|
302
|
+
if (!isViewport && !isRender) return;
|
|
303
|
+
|
|
304
|
+
let cancelled = false;
|
|
305
|
+
let unsubIdle: (() => void) | undefined;
|
|
306
|
+
let observedElement: Element | null = null;
|
|
307
|
+
|
|
308
|
+
const triggerPrefetch = () => {
|
|
309
|
+
if (cancelled) return;
|
|
310
|
+
const segmentState = ctx.store.getSegmentState();
|
|
311
|
+
prefetchQueued(to, segmentState.currentSegmentIds, ctx.version);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Schedule prefetch only when the app is idle (no navigation/streaming).
|
|
315
|
+
// This avoids competing with hydration and active navigation fetches.
|
|
316
|
+
const scheduleWhenIdle = (callback: () => void) => {
|
|
317
|
+
const state = ctx.eventController.getState();
|
|
318
|
+
if (state.state === "idle" && !state.isStreaming) {
|
|
319
|
+
callback();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const unsub = ctx.eventController.subscribe(() => {
|
|
323
|
+
const s = ctx.eventController.getState();
|
|
324
|
+
if (s.state === "idle" && !s.isStreaming) {
|
|
325
|
+
unsub();
|
|
326
|
+
callback();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
unsubIdle = unsub;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
if (isRender) {
|
|
333
|
+
scheduleWhenIdle(triggerPrefetch);
|
|
334
|
+
} else if (isViewport) {
|
|
335
|
+
const element = internalRef.current;
|
|
336
|
+
if (!element) return;
|
|
337
|
+
observedElement = element;
|
|
338
|
+
observeForPrefetch(element, () => {
|
|
339
|
+
scheduleWhenIdle(triggerPrefetch);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return () => {
|
|
344
|
+
cancelled = true;
|
|
345
|
+
unsubIdle?.();
|
|
346
|
+
if (isViewport && observedElement) {
|
|
347
|
+
unobserveForPrefetch(observedElement);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}, [resolvedStrategy, to, isExternal, ctx]);
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<a
|
|
354
|
+
ref={setRef}
|
|
355
|
+
href={to}
|
|
356
|
+
onClick={handleClick}
|
|
357
|
+
onMouseEnter={handleMouseEnter}
|
|
358
|
+
data-link-component
|
|
359
|
+
data-external={isExternal ? "" : undefined}
|
|
360
|
+
data-scroll={scroll === false ? "false" : undefined}
|
|
361
|
+
data-replace={replace ? "true" : undefined}
|
|
362
|
+
data-revalidate={revalidate === false ? "false" : undefined}
|
|
363
|
+
{...props}
|
|
364
|
+
>
|
|
365
|
+
<LinkContext.Provider value={to}>{children}</LinkContext.Provider>
|
|
366
|
+
</a>
|
|
367
|
+
);
|
|
368
|
+
});
|