@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.80
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 +942 -4
- package/dist/bin/rango.js +1689 -0
- package/dist/vite/index.js +4960 -935
- package/package.json +70 -60
- package/skills/breadcrumbs/SKILL.md +250 -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 +167 -0
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +334 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +151 -8
- package/skills/layout/SKILL.md +122 -3
- package/skills/links/SKILL.md +92 -31
- package/skills/loader/SKILL.md +404 -44
- package/skills/middleware/SKILL.md +205 -37
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +263 -1
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +87 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +281 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +328 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- 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/app-version.ts +14 -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 +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +317 -560
- package/src/browser/navigation-client.ts +206 -68
- package/src/browser/navigation-store.ts +73 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +343 -316
- package/src/browser/prefetch/cache.ts +216 -0
- package/src/browser/prefetch/fetch.ts +206 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +160 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +253 -74
- package/src/browser/react/NavigationProvider.tsx +87 -11
- package/src/browser/react/context.ts +11 -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 +30 -126
- package/src/browser/react/use-href.tsx +2 -2
- 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 +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +76 -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 +214 -58
- 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 +141 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +39 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +291 -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 +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 -309
- 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 +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +135 -301
- 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 +108 -2
- package/src/handle.ts +55 -29
- 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 +119 -29
- package/src/index.rsc.ts +155 -19
- package/src/index.ts +251 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -157
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +186 -0
- package/src/prerender.ts +524 -0
- package/src/reverse.ts +354 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1121 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +478 -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 +149 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +217 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +77 -8
- 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 +438 -86
- package/src/router/intercept-resolution.ts +402 -0
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +356 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +163 -35
- package/src/router/match-api.ts +555 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +460 -10
- package/src/router/match-middleware/cache-store.ts +98 -26
- package/src/router/match-middleware/intercept-resolution.ts +57 -17
- package/src/router/match-middleware/segment-resolution.ts +80 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +135 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +220 -0
- package/src/router/middleware.ts +324 -369
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +211 -43
- 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 +137 -38
- 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 +748 -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 +1379 -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 +78 -3
- package/src/router.ts +740 -4252
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +907 -797
- package/src/rsc/helpers.ts +140 -6
- 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 +391 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +246 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +356 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +46 -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 +134 -36
- package/src/server/context.ts +341 -61
- 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 +607 -81
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +103 -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 +791 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +210 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +150 -0
- package/src/types.ts +1 -1623
- 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 +116 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -802
- package/src/use-loader.tsx +161 -81
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +348 -0
- package/src/vite/discovery/prerender-collection.ts +439 -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 +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -1133
- package/src/vite/plugin-types.ts +103 -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 -53
- 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 +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +786 -0
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -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 +462 -0
- package/src/vite/router-discovery.ts +918 -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/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +221 -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/href.ts +0 -255
- package/src/server/route-manifest-cache.ts +0 -173
- 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/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Readiness
|
|
3
|
+
*
|
|
4
|
+
* Utilities to defer speculative prefetches until critical resources
|
|
5
|
+
* (viewport images) have finished loading. Prevents prefetch fetch()
|
|
6
|
+
* calls from competing with images for the browser's connection pool.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve when all in-viewport images have finished loading.
|
|
11
|
+
* Returns immediately if no images are pending.
|
|
12
|
+
*
|
|
13
|
+
* Only checks images that exist at call time — does not observe
|
|
14
|
+
* dynamically added images. For SPA navigations where new images
|
|
15
|
+
* appear after render, call this after the navigation settles.
|
|
16
|
+
*/
|
|
17
|
+
export function waitForViewportImages(): Promise<void> {
|
|
18
|
+
if (typeof document === "undefined") return Promise.resolve();
|
|
19
|
+
|
|
20
|
+
const pending = Array.from(document.querySelectorAll("img")).filter((img) => {
|
|
21
|
+
if (img.complete) return false;
|
|
22
|
+
const rect = img.getBoundingClientRect();
|
|
23
|
+
return (
|
|
24
|
+
rect.bottom > 0 &&
|
|
25
|
+
rect.right > 0 &&
|
|
26
|
+
rect.top < window.innerHeight &&
|
|
27
|
+
rect.left < window.innerWidth
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (pending.length === 0) return Promise.resolve();
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const settled = new Set<HTMLImageElement>();
|
|
35
|
+
|
|
36
|
+
const settle = (img: HTMLImageElement) => {
|
|
37
|
+
if (settled.has(img)) return;
|
|
38
|
+
settled.add(img);
|
|
39
|
+
if (settled.size >= pending.length) resolve();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
for (const img of pending) {
|
|
43
|
+
img.addEventListener("load", () => settle(img), { once: true });
|
|
44
|
+
img.addEventListener("error", () => settle(img), { once: true });
|
|
45
|
+
// Re-check: image may have completed between the initial filter
|
|
46
|
+
// and listener attachment. settle() is idempotent per image, so
|
|
47
|
+
// a queued load event firing afterward is harmless.
|
|
48
|
+
if (img.complete) settle(img);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve after the given number of milliseconds.
|
|
55
|
+
*/
|
|
56
|
+
export function wait(ms: number): Promise<void> {
|
|
57
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve when the browser has an idle main-thread moment.
|
|
62
|
+
* Uses requestIdleCallback where available, falls back to setTimeout.
|
|
63
|
+
*
|
|
64
|
+
* This is a scheduling hint, not an asset-loaded detector — combine
|
|
65
|
+
* with waitForViewportImages() for full resource readiness.
|
|
66
|
+
*/
|
|
67
|
+
export function waitForIdle(timeout = 200): Promise<void> {
|
|
68
|
+
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
window.requestIdleCallback(() => resolve(), { timeout });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
setTimeout(resolve, 0);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rango State
|
|
3
|
+
*
|
|
4
|
+
* Manages a localStorage-based state key for HTTP cache invalidation.
|
|
5
|
+
* The key is sent as the `X-Rango-State` header on both prefetch and
|
|
6
|
+
* navigation requests. The server responds with `Vary: X-Rango-State`,
|
|
7
|
+
* so the browser HTTP cache keys responses by (URL, X-Rango-State value).
|
|
8
|
+
*
|
|
9
|
+
* Format: `{buildVersion}:{invalidationTimestamp}`
|
|
10
|
+
* - Build version changes on deploy, busting all cached prefetches.
|
|
11
|
+
* - Timestamp changes on server action invalidation.
|
|
12
|
+
*
|
|
13
|
+
* localStorage is cross-tab and survives page refresh, so:
|
|
14
|
+
* - One tab's prefetch warms the cache for all tabs.
|
|
15
|
+
* - Invalidation in one tab is picked up by other tabs on next fetch.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const STORAGE_KEY = "rango-state";
|
|
19
|
+
|
|
20
|
+
// Module-level cache avoids hitting localStorage on every getRangoState() call.
|
|
21
|
+
// Initialized from localStorage on first access or by initRangoState().
|
|
22
|
+
let cachedState: string | null = null;
|
|
23
|
+
|
|
24
|
+
// Cross-tab sync: the `storage` event fires in OTHER tabs when one tab writes
|
|
25
|
+
// to localStorage, keeping cachedState fresh without polling.
|
|
26
|
+
let storageListenerAttached = false;
|
|
27
|
+
|
|
28
|
+
function attachStorageListener(): void {
|
|
29
|
+
if (storageListenerAttached || typeof window === "undefined") return;
|
|
30
|
+
window.addEventListener("storage", (e) => {
|
|
31
|
+
if (e.key !== STORAGE_KEY) return;
|
|
32
|
+
cachedState = e.newValue;
|
|
33
|
+
});
|
|
34
|
+
storageListenerAttached = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Initialize the Rango state key in localStorage.
|
|
39
|
+
* Called once at app startup with the build version from the server.
|
|
40
|
+
* If localStorage already has a key with matching version prefix, keeps it
|
|
41
|
+
* (preserves invalidation state across refresh). Otherwise writes a new key.
|
|
42
|
+
*/
|
|
43
|
+
export function initRangoState(version: string): void {
|
|
44
|
+
if (typeof window === "undefined") return;
|
|
45
|
+
|
|
46
|
+
attachStorageListener();
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const existing = localStorage.getItem(STORAGE_KEY);
|
|
50
|
+
if (existing) {
|
|
51
|
+
const colonIdx = existing.indexOf(":");
|
|
52
|
+
if (colonIdx > 0) {
|
|
53
|
+
const existingVersion = existing.slice(0, colonIdx);
|
|
54
|
+
if (existingVersion === version) {
|
|
55
|
+
cachedState = existing;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// New version or first load
|
|
61
|
+
const newState = `${version}:${Date.now()}`;
|
|
62
|
+
localStorage.setItem(STORAGE_KEY, newState);
|
|
63
|
+
cachedState = newState;
|
|
64
|
+
} catch {
|
|
65
|
+
// localStorage may be unavailable (private browsing in some browsers)
|
|
66
|
+
cachedState = `${version}:${Date.now()}`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get the current Rango state key value.
|
|
72
|
+
* Used as the `X-Rango-State` header value for prefetch and navigation requests.
|
|
73
|
+
*/
|
|
74
|
+
export function getRangoState(): string {
|
|
75
|
+
if (cachedState) return cachedState;
|
|
76
|
+
|
|
77
|
+
if (typeof window === "undefined") return "0:0";
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
81
|
+
if (stored) {
|
|
82
|
+
cachedState = stored;
|
|
83
|
+
return stored;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Fallback for unavailable localStorage
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return "0:0";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Invalidate the Rango state key. Called when server actions mutate data.
|
|
94
|
+
* Updates the timestamp portion while keeping the version prefix.
|
|
95
|
+
* The new value takes effect immediately for all subsequent fetches,
|
|
96
|
+
* causing Vary mismatches with previously cached responses.
|
|
97
|
+
*/
|
|
98
|
+
export function invalidateRangoState(): void {
|
|
99
|
+
const current = getRangoState();
|
|
100
|
+
const colonIdx = current.indexOf(":");
|
|
101
|
+
const version = colonIdx > 0 ? current.slice(0, colonIdx) : "0";
|
|
102
|
+
const newState = `${version}:${Date.now()}`;
|
|
103
|
+
cachedState = newState;
|
|
104
|
+
|
|
105
|
+
if (typeof window === "undefined") return;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
localStorage.setItem(STORAGE_KEY, newState);
|
|
109
|
+
} catch {
|
|
110
|
+
// Silently handle localStorage errors
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -1,69 +1,73 @@
|
|
|
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
|
-
* The URL to navigate to (typically from router.
|
|
70
|
+
* The URL to navigate to (typically from router.reverse())
|
|
67
71
|
*/
|
|
68
72
|
to: string;
|
|
69
73
|
/**
|
|
@@ -78,11 +82,41 @@ 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
|
+
* Custom prefetch cache key for source-agnostic cache reuse.
|
|
102
|
+
* When set, prefetch responses are cached independently of the current
|
|
103
|
+
* page URL, so navigating to the same target from different source pages
|
|
104
|
+
* reuses the cached prefetch.
|
|
105
|
+
*
|
|
106
|
+
* - String: static group name (e.g., `"pages"`)
|
|
107
|
+
* - Function: receives current URL (`window.location.href`), returns a
|
|
108
|
+
* normalized source key
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```tsx
|
|
112
|
+
* // Static group — all "pages" links share one cache entry per target
|
|
113
|
+
* <Link to="/page/3" prefetch="hover" prefetchKey="pages" />
|
|
114
|
+
*
|
|
115
|
+
* // Normalize — strip trailing page number from source URL
|
|
116
|
+
* <Link to="/page/3" prefetch="hover" prefetchKey={(from) => from.replace(/\/\d+$/, '')} />
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
prefetchKey?: string | ((from: string) => string);
|
|
86
120
|
/**
|
|
87
121
|
* State to pass to history.pushState/replaceState.
|
|
88
122
|
* Accessible via useLocationState() hook.
|
|
@@ -90,16 +124,29 @@ export interface LinkProps
|
|
|
90
124
|
* @example
|
|
91
125
|
* ```tsx
|
|
92
126
|
* // Type-safe state with createLocationState (recommended)
|
|
93
|
-
* const ProductState = createLocationState
|
|
94
|
-
* <Link to="/product" state={[ProductState(product)]}>
|
|
127
|
+
* const ProductState = createLocationState<{ name: string; price: number }>();
|
|
128
|
+
* <Link to="/product" state={[ProductState({ name: product.name, price: product.price })]}>
|
|
129
|
+
* View
|
|
130
|
+
* </Link>
|
|
131
|
+
*
|
|
132
|
+
* // Type-safe just-in-time state (getter called at click time, not render time).
|
|
133
|
+
* // Must be in a client component -- getter can't cross the RSC boundary.
|
|
134
|
+
* <Link
|
|
135
|
+
* to="/product"
|
|
136
|
+
* state={[ProductState(() => ({ name: product.name, price: product.price }))]}
|
|
137
|
+
* >
|
|
138
|
+
* View
|
|
139
|
+
* </Link>
|
|
95
140
|
*
|
|
96
141
|
* // Multiple typed states
|
|
97
|
-
* <Link to="/checkout" state={[ProductState(p), CartState(c)]}>
|
|
142
|
+
* <Link to="/checkout" state={[ProductState({ name: p.name, price: p.price }), CartState(c)]}>
|
|
143
|
+
* Checkout
|
|
144
|
+
* </Link>
|
|
98
145
|
*
|
|
99
|
-
* //
|
|
146
|
+
* // Plain static state
|
|
100
147
|
* <Link to="/product" state={{ from: "list" }}>View</Link>
|
|
101
148
|
*
|
|
102
|
-
* //
|
|
149
|
+
* // Plain just-in-time state (called at click time, requires client component)
|
|
103
150
|
* <Link to="/product" state={() => ({ scrollY: window.scrollY })}>View</Link>
|
|
104
151
|
* ```
|
|
105
152
|
*/
|
|
@@ -134,9 +181,9 @@ function isExternalUrl(href: string): boolean {
|
|
|
134
181
|
/**
|
|
135
182
|
* Type-safe Link component for SPA navigation
|
|
136
183
|
*
|
|
137
|
-
* Works with router.
|
|
184
|
+
* Works with router.reverse() for type-safe URLs:
|
|
138
185
|
* ```tsx
|
|
139
|
-
* <Link to={router.
|
|
186
|
+
* <Link to={router.reverse("shop.products.detail", { slug: "my-product" })}>
|
|
140
187
|
* View Product
|
|
141
188
|
* </Link>
|
|
142
189
|
* ```
|
|
@@ -147,23 +194,56 @@ function isExternalUrl(href: string): boolean {
|
|
|
147
194
|
* <Link to="https://example.com">External</Link>
|
|
148
195
|
* ```
|
|
149
196
|
*/
|
|
150
|
-
export const Link: ForwardRefExoticComponent<
|
|
197
|
+
export const Link: ForwardRefExoticComponent<
|
|
198
|
+
LinkProps & RefAttributes<HTMLAnchorElement>
|
|
199
|
+
> = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
|
151
200
|
{
|
|
152
201
|
to,
|
|
153
202
|
replace = false,
|
|
154
203
|
scroll = true,
|
|
155
204
|
reloadDocument = false,
|
|
205
|
+
revalidate,
|
|
156
206
|
prefetch = "none",
|
|
207
|
+
prefetchKey,
|
|
157
208
|
state,
|
|
158
209
|
children,
|
|
159
210
|
onClick,
|
|
160
211
|
...props
|
|
161
212
|
},
|
|
162
|
-
ref
|
|
213
|
+
ref,
|
|
163
214
|
) {
|
|
164
215
|
const ctx = useContext(NavigationStoreContext);
|
|
165
216
|
const isExternal = isExternalUrl(to);
|
|
166
217
|
|
|
218
|
+
// Auto-prefix with basename for app-local paths.
|
|
219
|
+
// Skip if external, already prefixed, or not a root-relative path.
|
|
220
|
+
const resolvedTo = useMemo(() => {
|
|
221
|
+
if (isExternal) return to;
|
|
222
|
+
const bn = ctx?.basename;
|
|
223
|
+
if (!bn || !to.startsWith("/") || to.startsWith(bn + "/") || to === bn)
|
|
224
|
+
return to;
|
|
225
|
+
return to === "/" ? bn : bn + to;
|
|
226
|
+
}, [to, isExternal, ctx?.basename]);
|
|
227
|
+
|
|
228
|
+
// Resolve adaptive: viewport on touch devices, hover on pointer devices
|
|
229
|
+
const resolvedStrategy =
|
|
230
|
+
prefetch === "adaptive" ? (isTouchDevice ? "viewport" : "hover") : prefetch;
|
|
231
|
+
|
|
232
|
+
// Internal ref for viewport observation; merge with forwarded ref
|
|
233
|
+
const internalRef = useRef<HTMLAnchorElement | null>(null);
|
|
234
|
+
const setRef = useCallback(
|
|
235
|
+
(node: HTMLAnchorElement | null) => {
|
|
236
|
+
internalRef.current = node;
|
|
237
|
+
if (typeof ref === "function") {
|
|
238
|
+
ref(node);
|
|
239
|
+
} else if (ref) {
|
|
240
|
+
(ref as React.MutableRefObject<HTMLAnchorElement | null>).current =
|
|
241
|
+
node;
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
[ref],
|
|
245
|
+
);
|
|
246
|
+
|
|
167
247
|
// Use ref to always get the latest state/getter without adding to useCallback deps
|
|
168
248
|
// This enables just-in-time state resolution without causing re-renders
|
|
169
249
|
const stateRef = useRef(state);
|
|
@@ -194,55 +274,154 @@ export const Link: ForwardRefExoticComponent<LinkProps & RefAttributes<HTMLAncho
|
|
|
194
274
|
const target = (e.currentTarget as HTMLAnchorElement).target;
|
|
195
275
|
if (target && target !== "_self") return;
|
|
196
276
|
|
|
277
|
+
// Hash-only navigation: let the browser handle anchor scrolling natively.
|
|
278
|
+
if (isHashOnlyNavigation(e.currentTarget as HTMLAnchorElement)) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// No navigation context (outside provider): fall back to native navigation.
|
|
283
|
+
if (!ctx?.navigate) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
197
287
|
// Prevent default and use SPA navigation
|
|
198
288
|
e.preventDefault();
|
|
199
289
|
// Stop propagation to prevent link-interceptor from also handling this
|
|
200
290
|
e.stopPropagation();
|
|
201
291
|
|
|
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
|
-
}
|
|
292
|
+
const currentState = stateRef.current;
|
|
293
|
+
let resolvedState: unknown;
|
|
217
294
|
|
|
218
|
-
|
|
295
|
+
if (
|
|
296
|
+
Array.isArray(currentState) &&
|
|
297
|
+
currentState.length > 0 &&
|
|
298
|
+
isLocationStateEntry(currentState[0])
|
|
299
|
+
) {
|
|
300
|
+
resolvedState = resolveLocationStateEntries(
|
|
301
|
+
currentState as LocationStateEntry[],
|
|
302
|
+
);
|
|
303
|
+
} else if (typeof currentState === "function") {
|
|
304
|
+
resolvedState = currentState();
|
|
305
|
+
} else if (currentState != null) {
|
|
306
|
+
resolvedState = currentState;
|
|
219
307
|
}
|
|
308
|
+
|
|
309
|
+
ctx.navigate(resolvedTo, {
|
|
310
|
+
replace,
|
|
311
|
+
scroll,
|
|
312
|
+
state: resolvedState,
|
|
313
|
+
revalidate,
|
|
314
|
+
});
|
|
220
315
|
},
|
|
221
|
-
[
|
|
316
|
+
[
|
|
317
|
+
resolvedTo,
|
|
318
|
+
isExternal,
|
|
319
|
+
reloadDocument,
|
|
320
|
+
replace,
|
|
321
|
+
scroll,
|
|
322
|
+
revalidate,
|
|
323
|
+
ctx,
|
|
324
|
+
onClick,
|
|
325
|
+
],
|
|
222
326
|
);
|
|
223
327
|
|
|
224
328
|
const handleMouseEnter = useCallback(() => {
|
|
225
|
-
if (
|
|
329
|
+
if (
|
|
330
|
+
(resolvedStrategy === "hover" || resolvedStrategy === "viewport") &&
|
|
331
|
+
!isExternal &&
|
|
332
|
+
ctx?.store
|
|
333
|
+
) {
|
|
334
|
+
// For "hover", this is the primary prefetch trigger.
|
|
335
|
+
// For "viewport", this upgrades/prioritizes a potentially queued
|
|
336
|
+
// prefetch — prefetchDirect bypasses the queue, and hasPrefetch
|
|
337
|
+
// deduplicates if the viewport prefetch already completed.
|
|
226
338
|
const segmentState = ctx.store.getSegmentState();
|
|
227
|
-
|
|
339
|
+
prefetchDirect(
|
|
340
|
+
resolvedTo,
|
|
341
|
+
segmentState.currentSegmentIds,
|
|
342
|
+
getAppVersion(),
|
|
343
|
+
ctx.store.getRouterId?.(),
|
|
344
|
+
prefetchKey,
|
|
345
|
+
);
|
|
228
346
|
}
|
|
229
|
-
}, [
|
|
347
|
+
}, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
|
|
348
|
+
|
|
349
|
+
// Viewport/render prefetch: waits for idle before starting,
|
|
350
|
+
// uses concurrency-limited queue to avoid flooding.
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
if (isExternal || !ctx?.store) return;
|
|
353
|
+
const isViewport = resolvedStrategy === "viewport";
|
|
354
|
+
const isRender = resolvedStrategy === "render";
|
|
355
|
+
if (!isViewport && !isRender) return;
|
|
356
|
+
|
|
357
|
+
let cancelled = false;
|
|
358
|
+
let unsubIdle: (() => void) | undefined;
|
|
359
|
+
let observedElement: Element | null = null;
|
|
360
|
+
|
|
361
|
+
const triggerPrefetch = () => {
|
|
362
|
+
if (cancelled) return;
|
|
363
|
+
const segmentState = ctx.store.getSegmentState();
|
|
364
|
+
prefetchQueued(
|
|
365
|
+
resolvedTo,
|
|
366
|
+
segmentState.currentSegmentIds,
|
|
367
|
+
getAppVersion(),
|
|
368
|
+
ctx.store.getRouterId?.(),
|
|
369
|
+
prefetchKey,
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Schedule prefetch only when the app is idle (no navigation/streaming).
|
|
374
|
+
// This avoids competing with hydration and active navigation fetches.
|
|
375
|
+
const scheduleWhenIdle = (callback: () => void) => {
|
|
376
|
+
const state = ctx.eventController.getState();
|
|
377
|
+
if (state.state === "idle" && !state.isStreaming) {
|
|
378
|
+
callback();
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const unsub = ctx.eventController.subscribe(() => {
|
|
382
|
+
const s = ctx.eventController.getState();
|
|
383
|
+
if (s.state === "idle" && !s.isStreaming) {
|
|
384
|
+
unsub();
|
|
385
|
+
callback();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
unsubIdle = unsub;
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
if (isRender) {
|
|
392
|
+
scheduleWhenIdle(triggerPrefetch);
|
|
393
|
+
} else if (isViewport) {
|
|
394
|
+
const element = internalRef.current;
|
|
395
|
+
if (!element) return;
|
|
396
|
+
observedElement = element;
|
|
397
|
+
observeForPrefetch(element, () => {
|
|
398
|
+
scheduleWhenIdle(triggerPrefetch);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return () => {
|
|
403
|
+
cancelled = true;
|
|
404
|
+
unsubIdle?.();
|
|
405
|
+
if (isViewport && observedElement) {
|
|
406
|
+
unobserveForPrefetch(observedElement);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}, [resolvedStrategy, resolvedTo, isExternal, ctx, prefetchKey]);
|
|
230
410
|
|
|
231
411
|
return (
|
|
232
412
|
<a
|
|
233
|
-
ref={
|
|
234
|
-
href={
|
|
413
|
+
ref={setRef}
|
|
414
|
+
href={resolvedTo}
|
|
235
415
|
onClick={handleClick}
|
|
236
416
|
onMouseEnter={handleMouseEnter}
|
|
237
417
|
data-link-component
|
|
238
418
|
data-external={isExternal ? "" : undefined}
|
|
239
419
|
data-scroll={scroll === false ? "false" : undefined}
|
|
240
420
|
data-replace={replace ? "true" : undefined}
|
|
421
|
+
data-revalidate={revalidate === false ? "false" : undefined}
|
|
241
422
|
{...props}
|
|
242
423
|
>
|
|
243
|
-
<LinkContext.Provider value={
|
|
244
|
-
{children}
|
|
245
|
-
</LinkContext.Provider>
|
|
424
|
+
<LinkContext.Provider value={resolvedTo}>{children}</LinkContext.Provider>
|
|
246
425
|
</a>
|
|
247
426
|
);
|
|
248
427
|
});
|