@rangojs/router 0.0.0-experimental.13 → 0.0.0-experimental.13221847
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 +884 -4
- package/dist/bin/rango.js +1531 -212
- package/dist/vite/index.js +3995 -2489
- package/package.json +57 -52
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +85 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +6 -4
- package/skills/hooks/SKILL.md +328 -70
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +131 -8
- package/skills/layout/SKILL.md +100 -3
- package/skills/links/SKILL.md +62 -15
- package/skills/loader/SKILL.md +368 -42
- package/skills/middleware/SKILL.md +171 -34
- package/skills/mime-routes/SKILL.md +14 -10
- package/skills/parallel/SKILL.md +137 -1
- package/skills/prerender/SKILL.md +366 -28
- package/skills/rango/SKILL.md +85 -21
- package/skills/response-routes/SKILL.md +136 -83
- package/skills/route/SKILL.md +195 -21
- package/skills/router-setup/SKILL.md +123 -30
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +240 -102
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +92 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +11 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +266 -558
- package/src/browser/navigation-client.ts +132 -75
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +303 -309
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +144 -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 +190 -70
- package/src/browser/react/NavigationProvider.tsx +78 -11
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- 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 +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +29 -70
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +22 -63
- 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 +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +188 -57
- package/src/browser/scroll-restoration.ts +117 -44
- package/src/browser/segment-reconciler.ts +221 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +488 -606
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +116 -47
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +63 -21
- package/src/build/generate-route-types.ts +36 -1038
- package/src/build/index.ts +2 -5
- package/src/build/route-trie.ts +38 -12
- 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 +342 -0
- package/src/cache/cache-scope.ts +122 -303
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- 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 +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +84 -126
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +77 -7
- package/src/handle.ts +12 -7
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +65 -45
- package/src/index.rsc.ts +104 -40
- package/src/index.ts +122 -67
- package/src/internal-debug.ts +9 -3
- package/src/loader.rsc.ts +18 -93
- package/src/loader.ts +26 -9
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +121 -17
- package/src/prerender.ts +325 -20
- package/src/reverse.ts +144 -124
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +959 -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 -1450
- package/src/route-map-builder.ts +87 -133
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +41 -6
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +324 -116
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +179 -133
- package/src/router/logging.ts +112 -6
- package/src/router/manifest.ts +58 -19
- package/src/router/match-api.ts +89 -88
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +86 -89
- package/src/router/match-middleware/cache-lookup.ts +295 -49
- package/src/router/match-middleware/cache-store.ts +56 -13
- package/src/router/match-middleware/intercept-resolution.ts +45 -22
- package/src/router/match-middleware/segment-resolution.ts +20 -9
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +44 -21
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +327 -369
- package/src/router/pattern-matching.ts +169 -31
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +105 -14
- package/src/router/router-context.ts +40 -21
- 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 +677 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1296 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1354
- 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 +96 -29
- package/src/router/types.ts +15 -9
- package/src/router.ts +642 -2366
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +639 -1027
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -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 +38 -11
- package/src/search-params.ts +66 -54
- package/src/segment-system.tsx +165 -17
- package/src/server/context.ts +237 -54
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +438 -71
- package/src/server.ts +26 -164
- package/src/ssr/index.tsx +101 -31
- package/src/static-handler.ts +22 -4
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- 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 +773 -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 +150 -0
- package/src/types.ts +1 -1795
- 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 -1323
- package/src/use-loader.tsx +85 -77
- 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 +11 -2259
- 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/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -47
- package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +8 -43
- 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/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- 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/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +181 -9
- 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/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
- package/CLAUDE.md +0 -43
- package/dist/vite/index.named-routes.gen.ts +0 -103
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-internal-ids.ts +0 -1167
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -1,67 +1,69 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React, {
|
|
3
|
+
import React, {
|
|
4
|
+
forwardRef,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
type ForwardRefExoticComponent,
|
|
10
|
+
type RefAttributes,
|
|
11
|
+
} from "react";
|
|
4
12
|
import { NavigationStoreContext } from "./context.js";
|
|
5
13
|
import { LinkContext } from "./use-link-status.js";
|
|
6
14
|
import type { NavigateOptions } from "../types.js";
|
|
15
|
+
import { isHashOnlyNavigation } from "../link-interceptor.js";
|
|
7
16
|
import {
|
|
8
|
-
type LocationStateEntry,
|
|
9
17
|
isLocationStateEntry,
|
|
18
|
+
type LocationStateEntry,
|
|
10
19
|
resolveLocationStateEntries,
|
|
11
20
|
} from "./location-state.js";
|
|
12
21
|
|
|
13
22
|
/**
|
|
14
|
-
* State
|
|
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
|
|
15
27
|
*/
|
|
16
28
|
export type StateOrGetter<T = unknown> = T | (() => T);
|
|
17
29
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* - StateOrGetter: Legacy format for backwards compatibility
|
|
22
|
-
*/
|
|
23
|
-
export type LinkState = LocationStateEntry[] | StateOrGetter;
|
|
24
|
-
|
|
25
|
-
// Track prefetched URLs to avoid duplicate <link> elements
|
|
26
|
-
const prefetchedUrls = new Set<string>();
|
|
30
|
+
export type LinkState =
|
|
31
|
+
| LocationStateEntry[]
|
|
32
|
+
| StateOrGetter<Record<string, unknown>>;
|
|
27
33
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
}
|
|
34
|
+
import { prefetchDirect, prefetchQueued } from "../prefetch/fetch.js";
|
|
35
|
+
import {
|
|
36
|
+
observeForPrefetch,
|
|
37
|
+
unobserveForPrefetch,
|
|
38
|
+
} from "../prefetch/observer.js";
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
link.as = "fetch";
|
|
48
|
-
document.head.appendChild(link);
|
|
49
|
-
}
|
|
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;
|
|
50
44
|
|
|
51
45
|
/**
|
|
52
46
|
* Prefetch strategy for the Link component
|
|
53
|
-
* - "hover": Prefetch on mouse enter (
|
|
54
|
-
* - "viewport": Prefetch when link enters viewport (
|
|
55
|
-
* - "
|
|
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
|
|
56
51
|
* - "none": No prefetching (default)
|
|
57
52
|
*/
|
|
58
|
-
export type PrefetchStrategy =
|
|
53
|
+
export type PrefetchStrategy =
|
|
54
|
+
| "hover"
|
|
55
|
+
| "viewport"
|
|
56
|
+
| "render"
|
|
57
|
+
| "adaptive"
|
|
58
|
+
| "none";
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* Link component props
|
|
62
62
|
*/
|
|
63
|
-
export interface LinkProps
|
|
64
|
-
|
|
63
|
+
export interface LinkProps extends Omit<
|
|
64
|
+
React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
65
|
+
"href"
|
|
66
|
+
> {
|
|
65
67
|
/**
|
|
66
68
|
* The URL to navigate to (typically from router.reverse())
|
|
67
69
|
*/
|
|
@@ -78,6 +80,16 @@ export interface LinkProps
|
|
|
78
80
|
* Force full document navigation instead of SPA
|
|
79
81
|
*/
|
|
80
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;
|
|
81
93
|
/**
|
|
82
94
|
* Prefetch strategy for the link destination
|
|
83
95
|
* @default "none"
|
|
@@ -90,16 +102,29 @@ export interface LinkProps
|
|
|
90
102
|
* @example
|
|
91
103
|
* ```tsx
|
|
92
104
|
* // Type-safe state with createLocationState (recommended)
|
|
93
|
-
* const ProductState = createLocationState
|
|
94
|
-
* <Link to="/product" state={[ProductState(product)]}>
|
|
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>
|
|
95
118
|
*
|
|
96
119
|
* // Multiple typed states
|
|
97
|
-
* <Link to="/checkout" state={[ProductState(p), CartState(c)]}>
|
|
120
|
+
* <Link to="/checkout" state={[ProductState({ name: p.name, price: p.price }), CartState(c)]}>
|
|
121
|
+
* Checkout
|
|
122
|
+
* </Link>
|
|
98
123
|
*
|
|
99
|
-
* //
|
|
124
|
+
* // Plain static state
|
|
100
125
|
* <Link to="/product" state={{ from: "list" }}>View</Link>
|
|
101
126
|
*
|
|
102
|
-
* //
|
|
127
|
+
* // Plain just-in-time state (called at click time, requires client component)
|
|
103
128
|
* <Link to="/product" state={() => ({ scrollY: window.scrollY })}>View</Link>
|
|
104
129
|
* ```
|
|
105
130
|
*/
|
|
@@ -147,23 +172,45 @@ function isExternalUrl(href: string): boolean {
|
|
|
147
172
|
* <Link to="https://example.com">External</Link>
|
|
148
173
|
* ```
|
|
149
174
|
*/
|
|
150
|
-
export const Link: ForwardRefExoticComponent<
|
|
175
|
+
export const Link: ForwardRefExoticComponent<
|
|
176
|
+
LinkProps & RefAttributes<HTMLAnchorElement>
|
|
177
|
+
> = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
|
151
178
|
{
|
|
152
179
|
to,
|
|
153
180
|
replace = false,
|
|
154
181
|
scroll = true,
|
|
155
182
|
reloadDocument = false,
|
|
183
|
+
revalidate,
|
|
156
184
|
prefetch = "none",
|
|
157
185
|
state,
|
|
158
186
|
children,
|
|
159
187
|
onClick,
|
|
160
188
|
...props
|
|
161
189
|
},
|
|
162
|
-
ref
|
|
190
|
+
ref,
|
|
163
191
|
) {
|
|
164
192
|
const ctx = useContext(NavigationStoreContext);
|
|
165
193
|
const isExternal = isExternalUrl(to);
|
|
166
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
|
+
|
|
167
214
|
// Use ref to always get the latest state/getter without adding to useCallback deps
|
|
168
215
|
// This enables just-in-time state resolution without causing re-renders
|
|
169
216
|
const stateRef = useRef(state);
|
|
@@ -194,43 +241,117 @@ export const Link: ForwardRefExoticComponent<LinkProps & RefAttributes<HTMLAncho
|
|
|
194
241
|
const target = (e.currentTarget as HTMLAnchorElement).target;
|
|
195
242
|
if (target && target !== "_self") return;
|
|
196
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
|
+
|
|
197
254
|
// Prevent default and use SPA navigation
|
|
198
255
|
e.preventDefault();
|
|
199
256
|
// Stop propagation to prevent link-interceptor from also handling this
|
|
200
257
|
e.stopPropagation();
|
|
201
258
|
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
}
|
|
259
|
+
const currentState = stateRef.current;
|
|
260
|
+
let resolvedState: unknown;
|
|
217
261
|
|
|
218
|
-
|
|
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;
|
|
219
274
|
}
|
|
275
|
+
|
|
276
|
+
ctx.navigate(to, { replace, scroll, state: resolvedState, revalidate });
|
|
220
277
|
},
|
|
221
|
-
[to, isExternal, reloadDocument, replace, scroll, ctx, onClick]
|
|
278
|
+
[to, isExternal, reloadDocument, replace, scroll, revalidate, ctx, onClick],
|
|
222
279
|
);
|
|
223
280
|
|
|
224
281
|
const handleMouseEnter = useCallback(() => {
|
|
225
|
-
if (
|
|
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;
|
|
226
310
|
const segmentState = ctx.store.getSegmentState();
|
|
227
|
-
|
|
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
|
+
});
|
|
228
341
|
}
|
|
229
|
-
|
|
342
|
+
|
|
343
|
+
return () => {
|
|
344
|
+
cancelled = true;
|
|
345
|
+
unsubIdle?.();
|
|
346
|
+
if (isViewport && observedElement) {
|
|
347
|
+
unobserveForPrefetch(observedElement);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}, [resolvedStrategy, to, isExternal, ctx]);
|
|
230
351
|
|
|
231
352
|
return (
|
|
232
353
|
<a
|
|
233
|
-
ref={
|
|
354
|
+
ref={setRef}
|
|
234
355
|
href={to}
|
|
235
356
|
onClick={handleClick}
|
|
236
357
|
onMouseEnter={handleMouseEnter}
|
|
@@ -238,11 +359,10 @@ export const Link: ForwardRefExoticComponent<LinkProps & RefAttributes<HTMLAncho
|
|
|
238
359
|
data-external={isExternal ? "" : undefined}
|
|
239
360
|
data-scroll={scroll === false ? "false" : undefined}
|
|
240
361
|
data-replace={replace ? "true" : undefined}
|
|
362
|
+
data-revalidate={revalidate === false ? "false" : undefined}
|
|
241
363
|
{...props}
|
|
242
364
|
>
|
|
243
|
-
<LinkContext.Provider value={to}>
|
|
244
|
-
{children}
|
|
245
|
-
</LinkContext.Provider>
|
|
365
|
+
<LinkContext.Provider value={to}>{children}</LinkContext.Provider>
|
|
246
366
|
</a>
|
|
247
367
|
);
|
|
248
368
|
});
|
|
@@ -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
|
-
|
|
19
|
+
NavigationUpdate,
|
|
18
20
|
NavigateOptions,
|
|
19
21
|
NavigationBridge,
|
|
20
22
|
} from "../types.js";
|
|
@@ -22,7 +24,10 @@ import type { EventController } from "../event-controller.js";
|
|
|
22
24
|
import { RootErrorBoundary } from "../../root-error-boundary.js";
|
|
23
25
|
import type { HandleData } from "../types.js";
|
|
24
26
|
import { ThemeProvider } from "../../theme/ThemeProvider.js";
|
|
27
|
+
import { NonceContext } from "./nonce-context.js";
|
|
25
28
|
import type { ResolvedThemeConfig, Theme } from "../../theme/types.js";
|
|
29
|
+
import { cancelAllPrefetches } from "../prefetch/queue.js";
|
|
30
|
+
import { handleNavigationEnd } from "../scroll-restoration.js";
|
|
26
31
|
|
|
27
32
|
/**
|
|
28
33
|
* Process handles from an async generator, updating the event controller
|
|
@@ -42,7 +47,7 @@ async function processHandles(
|
|
|
42
47
|
matched?: string[];
|
|
43
48
|
isPartial?: boolean;
|
|
44
49
|
historyKey: string;
|
|
45
|
-
}
|
|
50
|
+
},
|
|
46
51
|
): Promise<void> {
|
|
47
52
|
const { eventController, store, matched, isPartial, historyKey } = opts;
|
|
48
53
|
|
|
@@ -53,7 +58,7 @@ async function processHandles(
|
|
|
53
58
|
// the current route's breadcrumbs (e.g., quick popstate after clicking a link).
|
|
54
59
|
if (historyKey !== store.getHistoryKey()) {
|
|
55
60
|
console.log(
|
|
56
|
-
"[NavigationProvider] Stopping handle processing - user navigated away"
|
|
61
|
+
"[NavigationProvider] Stopping handle processing - user navigated away",
|
|
57
62
|
);
|
|
58
63
|
return;
|
|
59
64
|
}
|
|
@@ -100,9 +105,9 @@ export interface NavigationProviderProps {
|
|
|
100
105
|
eventController: EventController;
|
|
101
106
|
|
|
102
107
|
/**
|
|
103
|
-
* Initial
|
|
108
|
+
* Initial rendered tree + metadata from server payload
|
|
104
109
|
*/
|
|
105
|
-
initialPayload:
|
|
110
|
+
initialPayload: NavigationUpdate;
|
|
106
111
|
|
|
107
112
|
/**
|
|
108
113
|
* Navigation bridge for handling navigation
|
|
@@ -126,6 +131,12 @@ export interface NavigationProviderProps {
|
|
|
126
131
|
* When true, keeps TLS alive by sending HEAD requests after idle periods.
|
|
127
132
|
*/
|
|
128
133
|
warmupEnabled?: boolean;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* App version from server payload (stable, immutable).
|
|
137
|
+
* Forwarded to prefetch requests for version mismatch detection.
|
|
138
|
+
*/
|
|
139
|
+
version?: string;
|
|
129
140
|
}
|
|
130
141
|
|
|
131
142
|
/**
|
|
@@ -157,6 +168,7 @@ export function NavigationProvider({
|
|
|
157
168
|
themeConfig,
|
|
158
169
|
initialTheme,
|
|
159
170
|
warmupEnabled,
|
|
171
|
+
version,
|
|
160
172
|
}: NavigationProviderProps): ReactNode {
|
|
161
173
|
// Track current payload for rendering (this triggers re-renders)
|
|
162
174
|
const [payload, setPayload] = useState(initialPayload);
|
|
@@ -168,7 +180,7 @@ export function NavigationProvider({
|
|
|
168
180
|
async (url: string, options?: NavigateOptions): Promise<void> => {
|
|
169
181
|
await bridge.navigate(url, options);
|
|
170
182
|
},
|
|
171
|
-
[]
|
|
183
|
+
[],
|
|
172
184
|
);
|
|
173
185
|
|
|
174
186
|
/**
|
|
@@ -185,8 +197,9 @@ export function NavigationProvider({
|
|
|
185
197
|
eventController,
|
|
186
198
|
navigate,
|
|
187
199
|
refresh,
|
|
200
|
+
version,
|
|
188
201
|
}),
|
|
189
|
-
[]
|
|
202
|
+
[],
|
|
190
203
|
);
|
|
191
204
|
|
|
192
205
|
// Connection warmup: keep TLS alive after idle periods.
|
|
@@ -252,7 +265,12 @@ export function NavigationProvider({
|
|
|
252
265
|
}
|
|
253
266
|
|
|
254
267
|
// Activity events that reset the idle timer
|
|
255
|
-
const activityEvents = [
|
|
268
|
+
const activityEvents = [
|
|
269
|
+
"mousemove",
|
|
270
|
+
"keydown",
|
|
271
|
+
"touchstart",
|
|
272
|
+
"scroll",
|
|
273
|
+
] as const;
|
|
256
274
|
const activityOptions: AddEventListenerOptions = { passive: true };
|
|
257
275
|
|
|
258
276
|
for (const event of activityEvents) {
|
|
@@ -271,14 +289,56 @@ export function NavigationProvider({
|
|
|
271
289
|
};
|
|
272
290
|
}, [warmupEnabled]);
|
|
273
291
|
|
|
292
|
+
// Cancel speculative prefetches when navigation starts.
|
|
293
|
+
// Viewport/render prefetches should not compete with navigation fetches.
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
let wasIdle = true;
|
|
296
|
+
const unsub = eventController.subscribe(() => {
|
|
297
|
+
const state = eventController.getState();
|
|
298
|
+
const isIdle = state.state === "idle" && !state.isStreaming;
|
|
299
|
+
if (wasIdle && !isIdle) {
|
|
300
|
+
cancelAllPrefetches();
|
|
301
|
+
}
|
|
302
|
+
wasIdle = isIdle;
|
|
303
|
+
});
|
|
304
|
+
return unsub;
|
|
305
|
+
}, [eventController]);
|
|
306
|
+
|
|
307
|
+
// Pending scroll action to apply after React commits
|
|
308
|
+
const pendingScrollRef = useRef<NavigationUpdate["scroll"]>(undefined);
|
|
309
|
+
|
|
310
|
+
// Apply scroll after React commits the new content to the DOM
|
|
311
|
+
useLayoutEffect(() => {
|
|
312
|
+
const scrollAction = pendingScrollRef.current;
|
|
313
|
+
if (!scrollAction) return;
|
|
314
|
+
pendingScrollRef.current = undefined;
|
|
315
|
+
|
|
316
|
+
if (scrollAction.enabled === false) return;
|
|
317
|
+
|
|
318
|
+
handleNavigationEnd({
|
|
319
|
+
restore: scrollAction.restore,
|
|
320
|
+
scroll: scrollAction.enabled,
|
|
321
|
+
isStreaming: scrollAction.isStreaming,
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
274
325
|
// Subscribe to UI updates (for re-rendering the tree)
|
|
275
326
|
useEffect(() => {
|
|
276
327
|
const unsubscribe = store.onUpdate((update) => {
|
|
328
|
+
// Capture scroll intent — it will be applied in useLayoutEffect
|
|
329
|
+
// after React commits this state update to the DOM.
|
|
330
|
+
// Always assign (even undefined) to clear stale scroll from prior navigations,
|
|
331
|
+
// so server actions or error updates don't accidentally replay old scroll.
|
|
332
|
+
pendingScrollRef.current = update.scroll;
|
|
333
|
+
|
|
277
334
|
setPayload({
|
|
278
335
|
root: update.root,
|
|
279
336
|
metadata: update.metadata,
|
|
280
337
|
});
|
|
281
338
|
|
|
339
|
+
// Update route params
|
|
340
|
+
eventController.setParams(update.metadata.params ?? {});
|
|
341
|
+
|
|
282
342
|
// Update handle data progressively as it streams in
|
|
283
343
|
if (update.metadata.handles) {
|
|
284
344
|
// Capture historyKey now - by the time async processing completes,
|
|
@@ -292,7 +352,7 @@ export function NavigationProvider({
|
|
|
292
352
|
isPartial: update.metadata.isPartial,
|
|
293
353
|
historyKey,
|
|
294
354
|
}).catch((err) =>
|
|
295
|
-
console.error("[NavigationProvider] Error consuming handles:", err)
|
|
355
|
+
console.error("[NavigationProvider] Error consuming handles:", err),
|
|
296
356
|
);
|
|
297
357
|
} else if (update.metadata.cachedHandleData) {
|
|
298
358
|
// For back/forward navigation from cache, restore the cached handleData
|
|
@@ -300,14 +360,14 @@ export function NavigationProvider({
|
|
|
300
360
|
eventController.setHandleData(
|
|
301
361
|
update.metadata.cachedHandleData,
|
|
302
362
|
update.metadata.matched,
|
|
303
|
-
false // full replace - restore entire cached state
|
|
363
|
+
false, // full replace - restore entire cached state
|
|
304
364
|
);
|
|
305
365
|
} else if (update.metadata.matched) {
|
|
306
366
|
// For cached navigations without handleData, update segmentOrder to clean up stale data
|
|
307
367
|
eventController.setHandleData(
|
|
308
368
|
{}, // Empty data - all existing data not in matched will be cleaned up
|
|
309
369
|
update.metadata.matched,
|
|
310
|
-
true // partial update - will clean up segments not in matched
|
|
370
|
+
true, // partial update - will clean up segments not in matched
|
|
311
371
|
);
|
|
312
372
|
}
|
|
313
373
|
});
|
|
@@ -338,6 +398,13 @@ export function NavigationProvider({
|
|
|
338
398
|
);
|
|
339
399
|
}
|
|
340
400
|
|
|
401
|
+
// Match SSR tree shape: NonceContext.Provider is always present so
|
|
402
|
+
// hydration sees the same component tree. Value is undefined on the
|
|
403
|
+
// client — CSP nonces are a server-side HTML concern.
|
|
404
|
+
content = (
|
|
405
|
+
<NonceContext.Provider value={undefined}>{content}</NonceContext.Provider>
|
|
406
|
+
);
|
|
407
|
+
|
|
341
408
|
return (
|
|
342
409
|
<NavigationStoreContext.Provider value={contextValue}>
|
|
343
410
|
{content}
|
|
@@ -41,6 +41,12 @@ 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 server payload (stable, immutable).
|
|
47
|
+
* Used in prefetch requests for version mismatch detection.
|
|
48
|
+
*/
|
|
49
|
+
version: string | undefined;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
/**
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter segment IDs to only include routes and layouts.
|
|
3
|
+
* Excludes parallels (contain .@) and loaders (contain D followed by digit).
|
|
4
|
+
*/
|
|
5
|
+
export function filterSegmentOrder(matched: string[]): string[] {
|
|
6
|
+
return matched.filter((id) => {
|
|
7
|
+
if (id.includes(".@")) return false;
|
|
8
|
+
if (/D\d+\./.test(id)) return false;
|
|
9
|
+
return true;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
// React exports for browser navigation
|
|
2
2
|
|
|
3
3
|
// Hook with Zustand-style selectors
|
|
4
|
-
export {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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,
|
|
18
|
+
export { useSegments, type SegmentsState } from "./use-segments.js";
|
|
15
19
|
|
|
16
20
|
// Handle data hook
|
|
17
|
-
export { useHandle
|
|
21
|
+
export { useHandle } from "./use-handle.js";
|
|
18
22
|
|
|
19
23
|
// Client cache controls hook
|
|
20
24
|
export {
|
|
@@ -35,11 +39,7 @@ export {
|
|
|
35
39
|
} from "./context.js";
|
|
36
40
|
|
|
37
41
|
// Link component
|
|
38
|
-
export {
|
|
39
|
-
Link,
|
|
40
|
-
type LinkProps,
|
|
41
|
-
type PrefetchStrategy,
|
|
42
|
-
} from "./Link.js";
|
|
42
|
+
export { Link, type LinkProps, type PrefetchStrategy } from "./Link.js";
|
|
43
43
|
|
|
44
44
|
// Link status hook
|
|
45
45
|
export { useLinkStatus, type LinkStatus } from "./use-link-status.js";
|