@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -0
- package/README.md +1037 -4
- package/dist/bin/rango.js +1619 -157
- package/dist/vite/index.js +5762 -2301
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +71 -63
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -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/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +367 -71
- package/skills/host-router/SKILL.md +218 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +176 -8
- package/skills/layout/SKILL.md +124 -3
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +474 -47
- package/skills/middleware/SKILL.md +207 -37
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +15 -11
- package/skills/parallel/SKILL.md +272 -1
- package/skills/prerender/SKILL.md +467 -65
- package/skills/rango/SKILL.md +89 -21
- package/skills/response-routes/SKILL.md +152 -91
- package/skills/route/SKILL.md +305 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +333 -86
- package/skills/use-cache/SKILL.md +324 -0
- package/skills/view-transitions/SKILL.md +212 -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/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +136 -68
- 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 +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +374 -561
- package/src/browser/navigation-client.ts +228 -70
- package/src/browser/navigation-store.ts +97 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +376 -315
- package/src/browser/prefetch/cache.ts +314 -0
- package/src/browser/prefetch/fetch.ts +282 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +191 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +152 -0
- package/src/browser/react/Link.tsx +255 -71
- package/src/browser/react/NavigationProvider.tsx +152 -24
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +55 -0
- package/src/browser/react/index.ts +15 -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 +30 -120
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- package/src/browser/react/use-params.ts +78 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +83 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +85 -99
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +246 -64
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +158 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +84 -23
- package/src/build/generate-route-types.ts +39 -828
- package/src/build/index.ts +4 -5
- package/src/build/route-trie.ts +85 -32
- 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 +418 -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 +618 -0
- package/src/build/route-types/scan-filter.ts +85 -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 +167 -307
- package/src/cache/cf/cf-cache-store.ts +573 -21
- 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 +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +6 -1
- package/src/client.tsx +118 -302
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +77 -7
- package/src/handle.ts +55 -10
- 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 +138 -21
- package/src/index.ts +206 -51
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +25 -143
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +159 -13
- package/src/prerender.ts +397 -29
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +231 -121
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1134 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +483 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition.ts +1 -1431
- package/src/route-map-builder.ts +162 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +66 -9
- package/src/router/content-negotiation.ts +215 -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 +418 -86
- package/src/router/intercept-resolution.ts +35 -20
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +359 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +98 -32
- package/src/router/match-api.ts +196 -261
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +441 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +415 -86
- package/src/router/match-middleware/cache-store.ts +91 -29
- package/src/router/match-middleware/intercept-resolution.ts +48 -21
- package/src/router/match-middleware/segment-resolution.ts +73 -9
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +154 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +209 -0
- package/src/router/middleware.ts +373 -371
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +292 -52
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +152 -39
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +756 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1407 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1315
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/substitute-pattern-params.ts +56 -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 +111 -39
- package/src/router/types.ts +17 -9
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +642 -2011
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +864 -1114
- package/src/rsc/helpers.ts +181 -19
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +395 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +360 -0
- package/src/rsc/rsc-rendering.ts +256 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +360 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +52 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +187 -38
- package/src/server/context.ts +333 -59
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +603 -109
- package/src/server.ts +35 -155
- package/src/ssr/index.tsx +107 -30
- package/src/static-handler.ts +126 -0
- 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 +764 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +209 -0
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +167 -0
- package/src/types.ts +1 -1757
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +108 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -1282
- package/src/use-loader.tsx +161 -81
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +376 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +486 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -2063
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +98 -0
- package/src/vite/plugins/client-ref-dedup.ts +131 -0
- package/src/vite/plugins/client-ref-hashing.ts +117 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +816 -0
- package/src/vite/plugins/performance-tracks.ts +96 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +336 -0
- package/src/vite/plugins/version-injector.ts +109 -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 +497 -0
- package/src/vite/router-discovery.ts +1423 -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 +161 -0
- package/src/vite/utils/prerender-utils.ts +222 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- 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/urls.gen.ts +0 -8
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- package/src/vite/expose-prerender-handler-id.ts +0 -429
- package/src/vite/package-resolution.ts +0 -125
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -1,67 +1,71 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React, {
|
|
3
|
+
import React, {
|
|
4
|
+
forwardRef,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
type ForwardRefExoticComponent,
|
|
11
|
+
type RefAttributes,
|
|
12
|
+
} from "react";
|
|
4
13
|
import { NavigationStoreContext } from "./context.js";
|
|
5
14
|
import { LinkContext } from "./use-link-status.js";
|
|
6
15
|
import type { NavigateOptions } from "../types.js";
|
|
16
|
+
import { isHashOnlyNavigation } from "../link-interceptor.js";
|
|
7
17
|
import {
|
|
8
|
-
type LocationStateEntry,
|
|
9
18
|
isLocationStateEntry,
|
|
19
|
+
type LocationStateEntry,
|
|
10
20
|
resolveLocationStateEntries,
|
|
11
21
|
} from "./location-state.js";
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
|
-
* State
|
|
24
|
+
* State prop type for Link component.
|
|
25
|
+
* - LocationStateEntry[]: Type-safe state entries via createLocationState()
|
|
26
|
+
* - StateOrGetter: Plain state object or click-time getter function
|
|
27
|
+
* - Record<string, unknown>: Plain state object passed to history.pushState
|
|
15
28
|
*/
|
|
16
29
|
export type StateOrGetter<T = unknown> = T | (() => T);
|
|
17
30
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* - StateOrGetter: Legacy format for backwards compatibility
|
|
22
|
-
*/
|
|
23
|
-
export type LinkState = LocationStateEntry[] | StateOrGetter;
|
|
31
|
+
export type LinkState =
|
|
32
|
+
| LocationStateEntry[]
|
|
33
|
+
| StateOrGetter<Record<string, unknown>>;
|
|
24
34
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*/
|
|
32
|
-
function prefetchUrl(url: string, segmentIds: string[]): void {
|
|
33
|
-
if (prefetchedUrls.has(url)) return;
|
|
34
|
-
prefetchedUrls.add(url);
|
|
35
|
-
|
|
36
|
-
// Build RSC partial URL with segment IDs
|
|
37
|
-
const targetUrl = new URL(url, window.location.origin);
|
|
38
|
-
targetUrl.searchParams.set("_rsc_partial", "true");
|
|
39
|
-
if (segmentIds.length > 0) {
|
|
40
|
-
targetUrl.searchParams.set("_rsc_segments", segmentIds.join(","));
|
|
41
|
-
}
|
|
35
|
+
import { prefetchDirect, prefetchQueued } from "../prefetch/fetch.js";
|
|
36
|
+
import { getAppVersion } from "../app-version.js";
|
|
37
|
+
import {
|
|
38
|
+
observeForPrefetch,
|
|
39
|
+
unobserveForPrefetch,
|
|
40
|
+
} from "../prefetch/observer.js";
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
link.as = "fetch";
|
|
48
|
-
document.head.appendChild(link);
|
|
49
|
-
}
|
|
42
|
+
// Touch device detection for adaptive strategy.
|
|
43
|
+
// Checked once at module load (Link.tsx is "use client", runs only in browser).
|
|
44
|
+
const isTouchDevice =
|
|
45
|
+
typeof window !== "undefined" && window.matchMedia("(hover: none)").matches;
|
|
50
46
|
|
|
51
47
|
/**
|
|
52
48
|
* Prefetch strategy for the Link component
|
|
53
|
-
* - "hover": Prefetch on mouse enter (
|
|
54
|
-
* - "viewport": Prefetch when link enters viewport (
|
|
55
|
-
* - "
|
|
49
|
+
* - "hover": Prefetch on mouse enter (direct, no queue)
|
|
50
|
+
* - "viewport": Prefetch when link enters viewport (queued, waits for idle)
|
|
51
|
+
* - "render": Prefetch on component mount regardless of visibility (queued, waits for idle)
|
|
52
|
+
* - "adaptive": Hover on pointer devices, viewport on touch devices
|
|
56
53
|
* - "none": No prefetching (default)
|
|
57
54
|
*/
|
|
58
|
-
export type PrefetchStrategy =
|
|
55
|
+
export type PrefetchStrategy =
|
|
56
|
+
| "hover"
|
|
57
|
+
| "viewport"
|
|
58
|
+
| "render"
|
|
59
|
+
| "adaptive"
|
|
60
|
+
| "none";
|
|
59
61
|
|
|
60
62
|
/**
|
|
61
63
|
* Link component props
|
|
62
64
|
*/
|
|
63
|
-
export interface LinkProps
|
|
64
|
-
|
|
65
|
+
export interface LinkProps extends Omit<
|
|
66
|
+
React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
67
|
+
"href"
|
|
68
|
+
> {
|
|
65
69
|
/**
|
|
66
70
|
* The URL to navigate to (typically from router.reverse())
|
|
67
71
|
*/
|
|
@@ -78,11 +82,46 @@ export interface LinkProps
|
|
|
78
82
|
* Force full document navigation instead of SPA
|
|
79
83
|
*/
|
|
80
84
|
reloadDocument?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Whether to revalidate server data on navigation.
|
|
87
|
+
* Set to `false` to skip the RSC server fetch and only update the URL.
|
|
88
|
+
*
|
|
89
|
+
* Only takes effect when the pathname stays the same (search param / hash changes).
|
|
90
|
+
* If the pathname changes, this option is ignored and a full navigation occurs.
|
|
91
|
+
*
|
|
92
|
+
* @default true
|
|
93
|
+
*/
|
|
94
|
+
revalidate?: boolean;
|
|
81
95
|
/**
|
|
82
96
|
* Prefetch strategy for the link destination
|
|
83
97
|
* @default "none"
|
|
84
98
|
*/
|
|
85
99
|
prefetch?: PrefetchStrategy;
|
|
100
|
+
/**
|
|
101
|
+
* Opt-in override for the prefetch cache scope.
|
|
102
|
+
*
|
|
103
|
+
* The default cache is source-agnostic: one shared entry per target,
|
|
104
|
+
* keyed on Rango state + target URL. This is correct for routes whose
|
|
105
|
+
* response shape doesn't depend on where the user navigates from.
|
|
106
|
+
*
|
|
107
|
+
* Set `":source"` when this Link's response would legitimately differ
|
|
108
|
+
* based on the source page — typically when the target route (or one
|
|
109
|
+
* of its layouts) uses a custom `revalidate()` handler that reads
|
|
110
|
+
* `currentUrl` / `currentParams`, and the wildcard entry would
|
|
111
|
+
* therefore serve the wrong diff to a navigation from a different
|
|
112
|
+
* source.
|
|
113
|
+
*
|
|
114
|
+
* Intercept responses are auto-scoped to the source via a server-side
|
|
115
|
+
* tag, so `":source"` is only needed for custom revalidation logic.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```tsx
|
|
119
|
+
* // Route uses a `revalidate()` that branches on currentUrl — opt in
|
|
120
|
+
* // so prefetches don't bleed across source pages.
|
|
121
|
+
* <Link to="/dashboard" prefetch="hover" prefetchKey=":source" />
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
prefetchKey?: ":source";
|
|
86
125
|
/**
|
|
87
126
|
* State to pass to history.pushState/replaceState.
|
|
88
127
|
* Accessible via useLocationState() hook.
|
|
@@ -90,16 +129,29 @@ export interface LinkProps
|
|
|
90
129
|
* @example
|
|
91
130
|
* ```tsx
|
|
92
131
|
* // Type-safe state with createLocationState (recommended)
|
|
93
|
-
* const ProductState = createLocationState
|
|
94
|
-
* <Link to="/product" state={[ProductState(product)]}>
|
|
132
|
+
* const ProductState = createLocationState<{ name: string; price: number }>();
|
|
133
|
+
* <Link to="/product" state={[ProductState({ name: product.name, price: product.price })]}>
|
|
134
|
+
* View
|
|
135
|
+
* </Link>
|
|
136
|
+
*
|
|
137
|
+
* // Type-safe just-in-time state (getter called at click time, not render time).
|
|
138
|
+
* // Must be in a client component -- getter can't cross the RSC boundary.
|
|
139
|
+
* <Link
|
|
140
|
+
* to="/product"
|
|
141
|
+
* state={[ProductState(() => ({ name: product.name, price: product.price }))]}
|
|
142
|
+
* >
|
|
143
|
+
* View
|
|
144
|
+
* </Link>
|
|
95
145
|
*
|
|
96
146
|
* // Multiple typed states
|
|
97
|
-
* <Link to="/checkout" state={[ProductState(p), CartState(c)]}>
|
|
147
|
+
* <Link to="/checkout" state={[ProductState({ name: p.name, price: p.price }), CartState(c)]}>
|
|
148
|
+
* Checkout
|
|
149
|
+
* </Link>
|
|
98
150
|
*
|
|
99
|
-
* //
|
|
151
|
+
* // Plain static state
|
|
100
152
|
* <Link to="/product" state={{ from: "list" }}>View</Link>
|
|
101
153
|
*
|
|
102
|
-
* //
|
|
154
|
+
* // Plain just-in-time state (called at click time, requires client component)
|
|
103
155
|
* <Link to="/product" state={() => ({ scrollY: window.scrollY })}>View</Link>
|
|
104
156
|
* ```
|
|
105
157
|
*/
|
|
@@ -147,23 +199,56 @@ function isExternalUrl(href: string): boolean {
|
|
|
147
199
|
* <Link to="https://example.com">External</Link>
|
|
148
200
|
* ```
|
|
149
201
|
*/
|
|
150
|
-
export const Link: ForwardRefExoticComponent<
|
|
202
|
+
export const Link: ForwardRefExoticComponent<
|
|
203
|
+
LinkProps & RefAttributes<HTMLAnchorElement>
|
|
204
|
+
> = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
|
151
205
|
{
|
|
152
206
|
to,
|
|
153
207
|
replace = false,
|
|
154
208
|
scroll = true,
|
|
155
209
|
reloadDocument = false,
|
|
210
|
+
revalidate,
|
|
156
211
|
prefetch = "none",
|
|
212
|
+
prefetchKey,
|
|
157
213
|
state,
|
|
158
214
|
children,
|
|
159
215
|
onClick,
|
|
160
216
|
...props
|
|
161
217
|
},
|
|
162
|
-
ref
|
|
218
|
+
ref,
|
|
163
219
|
) {
|
|
164
220
|
const ctx = useContext(NavigationStoreContext);
|
|
165
221
|
const isExternal = isExternalUrl(to);
|
|
166
222
|
|
|
223
|
+
// Auto-prefix with basename for app-local paths.
|
|
224
|
+
// Skip if external, already prefixed, or not a root-relative path.
|
|
225
|
+
const resolvedTo = useMemo(() => {
|
|
226
|
+
if (isExternal) return to;
|
|
227
|
+
const bn = ctx?.basename;
|
|
228
|
+
if (!bn || !to.startsWith("/") || to.startsWith(bn + "/") || to === bn)
|
|
229
|
+
return to;
|
|
230
|
+
return to === "/" ? bn : bn + to;
|
|
231
|
+
}, [to, isExternal, ctx?.basename]);
|
|
232
|
+
|
|
233
|
+
// Resolve adaptive: viewport on touch devices, hover on pointer devices
|
|
234
|
+
const resolvedStrategy =
|
|
235
|
+
prefetch === "adaptive" ? (isTouchDevice ? "viewport" : "hover") : prefetch;
|
|
236
|
+
|
|
237
|
+
// Internal ref for viewport observation; merge with forwarded ref
|
|
238
|
+
const internalRef = useRef<HTMLAnchorElement | null>(null);
|
|
239
|
+
const setRef = useCallback(
|
|
240
|
+
(node: HTMLAnchorElement | null) => {
|
|
241
|
+
internalRef.current = node;
|
|
242
|
+
if (typeof ref === "function") {
|
|
243
|
+
ref(node);
|
|
244
|
+
} else if (ref) {
|
|
245
|
+
(ref as React.MutableRefObject<HTMLAnchorElement | null>).current =
|
|
246
|
+
node;
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
[ref],
|
|
250
|
+
);
|
|
251
|
+
|
|
167
252
|
// Use ref to always get the latest state/getter without adding to useCallback deps
|
|
168
253
|
// This enables just-in-time state resolution without causing re-renders
|
|
169
254
|
const stateRef = useRef(state);
|
|
@@ -194,55 +279,154 @@ export const Link: ForwardRefExoticComponent<LinkProps & RefAttributes<HTMLAncho
|
|
|
194
279
|
const target = (e.currentTarget as HTMLAnchorElement).target;
|
|
195
280
|
if (target && target !== "_self") return;
|
|
196
281
|
|
|
282
|
+
// Hash-only navigation: let the browser handle anchor scrolling natively.
|
|
283
|
+
if (isHashOnlyNavigation(e.currentTarget as HTMLAnchorElement)) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// No navigation context (outside provider): fall back to native navigation.
|
|
288
|
+
if (!ctx?.navigate) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
197
292
|
// Prevent default and use SPA navigation
|
|
198
293
|
e.preventDefault();
|
|
199
294
|
// Stop propagation to prevent link-interceptor from also handling this
|
|
200
295
|
e.stopPropagation();
|
|
201
296
|
|
|
202
|
-
|
|
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
|
-
}
|
|
297
|
+
const currentState = stateRef.current;
|
|
298
|
+
let resolvedState: unknown;
|
|
217
299
|
|
|
218
|
-
|
|
300
|
+
if (
|
|
301
|
+
Array.isArray(currentState) &&
|
|
302
|
+
currentState.length > 0 &&
|
|
303
|
+
isLocationStateEntry(currentState[0])
|
|
304
|
+
) {
|
|
305
|
+
resolvedState = resolveLocationStateEntries(
|
|
306
|
+
currentState as LocationStateEntry[],
|
|
307
|
+
);
|
|
308
|
+
} else if (typeof currentState === "function") {
|
|
309
|
+
resolvedState = currentState();
|
|
310
|
+
} else if (currentState != null) {
|
|
311
|
+
resolvedState = currentState;
|
|
219
312
|
}
|
|
313
|
+
|
|
314
|
+
ctx.navigate(resolvedTo, {
|
|
315
|
+
replace,
|
|
316
|
+
scroll,
|
|
317
|
+
state: resolvedState,
|
|
318
|
+
revalidate,
|
|
319
|
+
});
|
|
220
320
|
},
|
|
221
|
-
[
|
|
321
|
+
[
|
|
322
|
+
resolvedTo,
|
|
323
|
+
isExternal,
|
|
324
|
+
reloadDocument,
|
|
325
|
+
replace,
|
|
326
|
+
scroll,
|
|
327
|
+
revalidate,
|
|
328
|
+
ctx,
|
|
329
|
+
onClick,
|
|
330
|
+
],
|
|
222
331
|
);
|
|
223
332
|
|
|
224
333
|
const handleMouseEnter = useCallback(() => {
|
|
225
|
-
if (
|
|
334
|
+
if (
|
|
335
|
+
(resolvedStrategy === "hover" || resolvedStrategy === "viewport") &&
|
|
336
|
+
!isExternal &&
|
|
337
|
+
ctx?.store
|
|
338
|
+
) {
|
|
339
|
+
// For "hover", this is the primary prefetch trigger.
|
|
340
|
+
// For "viewport", this upgrades/prioritizes a potentially queued
|
|
341
|
+
// prefetch — prefetchDirect bypasses the queue, and hasPrefetch
|
|
342
|
+
// deduplicates if the viewport prefetch already completed.
|
|
226
343
|
const segmentState = ctx.store.getSegmentState();
|
|
227
|
-
|
|
344
|
+
prefetchDirect(
|
|
345
|
+
resolvedTo,
|
|
346
|
+
segmentState.currentSegmentIds,
|
|
347
|
+
getAppVersion(),
|
|
348
|
+
ctx.store.getRouterId?.(),
|
|
349
|
+
prefetchKey,
|
|
350
|
+
);
|
|
228
351
|
}
|
|
229
|
-
}, [
|
|
352
|
+
}, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
|
|
353
|
+
|
|
354
|
+
// Viewport/render prefetch: waits for idle before starting,
|
|
355
|
+
// uses concurrency-limited queue to avoid flooding.
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
if (isExternal || !ctx?.store) return;
|
|
358
|
+
const isViewport = resolvedStrategy === "viewport";
|
|
359
|
+
const isRender = resolvedStrategy === "render";
|
|
360
|
+
if (!isViewport && !isRender) return;
|
|
361
|
+
|
|
362
|
+
let cancelled = false;
|
|
363
|
+
let unsubIdle: (() => void) | undefined;
|
|
364
|
+
let observedElement: Element | null = null;
|
|
365
|
+
|
|
366
|
+
const triggerPrefetch = () => {
|
|
367
|
+
if (cancelled) return;
|
|
368
|
+
const segmentState = ctx.store.getSegmentState();
|
|
369
|
+
prefetchQueued(
|
|
370
|
+
resolvedTo,
|
|
371
|
+
segmentState.currentSegmentIds,
|
|
372
|
+
getAppVersion(),
|
|
373
|
+
ctx.store.getRouterId?.(),
|
|
374
|
+
prefetchKey,
|
|
375
|
+
);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Schedule prefetch only when the app is idle (no navigation/streaming).
|
|
379
|
+
// This avoids competing with hydration and active navigation fetches.
|
|
380
|
+
const scheduleWhenIdle = (callback: () => void) => {
|
|
381
|
+
const state = ctx.eventController.getState();
|
|
382
|
+
if (state.state === "idle" && !state.isStreaming) {
|
|
383
|
+
callback();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const unsub = ctx.eventController.subscribe(() => {
|
|
387
|
+
const s = ctx.eventController.getState();
|
|
388
|
+
if (s.state === "idle" && !s.isStreaming) {
|
|
389
|
+
unsub();
|
|
390
|
+
callback();
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
unsubIdle = unsub;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
if (isRender) {
|
|
397
|
+
scheduleWhenIdle(triggerPrefetch);
|
|
398
|
+
} else if (isViewport) {
|
|
399
|
+
const element = internalRef.current;
|
|
400
|
+
if (!element) return;
|
|
401
|
+
observedElement = element;
|
|
402
|
+
observeForPrefetch(element, () => {
|
|
403
|
+
scheduleWhenIdle(triggerPrefetch);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return () => {
|
|
408
|
+
cancelled = true;
|
|
409
|
+
unsubIdle?.();
|
|
410
|
+
if (isViewport && observedElement) {
|
|
411
|
+
unobserveForPrefetch(observedElement);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
}, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
|
|
230
415
|
|
|
231
416
|
return (
|
|
232
417
|
<a
|
|
233
|
-
ref={
|
|
234
|
-
href={
|
|
418
|
+
ref={setRef}
|
|
419
|
+
href={resolvedTo}
|
|
235
420
|
onClick={handleClick}
|
|
236
421
|
onMouseEnter={handleMouseEnter}
|
|
237
422
|
data-link-component
|
|
238
423
|
data-external={isExternal ? "" : undefined}
|
|
239
424
|
data-scroll={scroll === false ? "false" : undefined}
|
|
240
425
|
data-replace={replace ? "true" : undefined}
|
|
426
|
+
data-revalidate={revalidate === false ? "false" : undefined}
|
|
241
427
|
{...props}
|
|
242
428
|
>
|
|
243
|
-
<LinkContext.Provider value={
|
|
244
|
-
{children}
|
|
245
|
-
</LinkContext.Provider>
|
|
429
|
+
<LinkContext.Provider value={resolvedTo}>{children}</LinkContext.Provider>
|
|
246
430
|
</a>
|
|
247
431
|
);
|
|
248
432
|
});
|