vinext 0.0.40 → 0.0.42
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/README.md +1 -2
- package/dist/build/client-build-config.d.ts +119 -0
- package/dist/build/client-build-config.js +149 -0
- package/dist/build/client-build-config.js.map +1 -0
- package/dist/build/layout-classification-types.d.ts +62 -0
- package/dist/build/layout-classification-types.js +1 -0
- package/dist/build/layout-classification.d.ts +60 -0
- package/dist/build/layout-classification.js +98 -0
- package/dist/build/layout-classification.js.map +1 -0
- package/dist/build/report.d.ts +15 -1
- package/dist/build/report.js +50 -1
- package/dist/build/report.js.map +1 -1
- package/dist/build/route-classification-manifest.d.ts +53 -0
- package/dist/build/route-classification-manifest.js +145 -0
- package/dist/build/route-classification-manifest.js.map +1 -0
- package/dist/build/run-prerender.js +1 -1
- package/dist/build/ssr-manifest.d.ts +19 -0
- package/dist/build/ssr-manifest.js +71 -0
- package/dist/build/ssr-manifest.js.map +1 -0
- package/dist/check.js +4 -4
- package/dist/check.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/client/entry.js +1 -1
- package/dist/config/config-matchers.js +1 -0
- package/dist/config/config-matchers.js.map +1 -1
- package/dist/entries/app-rsc-entry.js +341 -114
- package/dist/entries/app-rsc-entry.js.map +1 -1
- package/dist/entries/pages-server-entry.js +205 -199
- package/dist/entries/pages-server-entry.js.map +1 -1
- package/dist/index.d.ts +1 -169
- package/dist/index.js +113 -432
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +1 -1
- package/dist/init.js +2 -2
- package/dist/init.js.map +1 -1
- package/dist/plugins/fonts.d.ts +49 -1
- package/dist/plugins/fonts.js +97 -3
- package/dist/plugins/fonts.js.map +1 -1
- package/dist/plugins/postcss.d.ts +27 -0
- package/dist/plugins/postcss.js +94 -0
- package/dist/plugins/postcss.js.map +1 -0
- package/dist/plugins/strip-server-exports.d.ts +14 -0
- package/dist/plugins/strip-server-exports.js +73 -0
- package/dist/plugins/strip-server-exports.js.map +1 -0
- package/dist/routing/app-router.d.ts +6 -4
- package/dist/routing/app-router.js +21 -22
- package/dist/routing/app-router.js.map +1 -1
- package/dist/server/app-browser-entry.js +235 -97
- package/dist/server/app-browser-entry.js.map +1 -1
- package/dist/server/app-browser-error.d.ts +8 -0
- package/dist/server/app-browser-error.js +9 -0
- package/dist/server/app-browser-error.js.map +1 -0
- package/dist/server/app-browser-state.d.ts +93 -0
- package/dist/server/app-browser-state.js +132 -0
- package/dist/server/app-browser-state.js.map +1 -0
- package/dist/server/app-elements.d.ts +92 -0
- package/dist/server/app-elements.js +122 -0
- package/dist/server/app-elements.js.map +1 -0
- package/dist/server/app-page-boundary-render.d.ts +3 -1
- package/dist/server/app-page-boundary-render.js +41 -1
- package/dist/server/app-page-boundary-render.js.map +1 -1
- package/dist/server/app-page-cache.d.ts +6 -3
- package/dist/server/app-page-cache.js +14 -8
- package/dist/server/app-page-cache.js.map +1 -1
- package/dist/server/app-page-execution.d.ts +36 -3
- package/dist/server/app-page-execution.js +50 -10
- package/dist/server/app-page-execution.js.map +1 -1
- package/dist/server/app-page-probe.d.ts +10 -4
- package/dist/server/app-page-probe.js +24 -15
- package/dist/server/app-page-probe.js.map +1 -1
- package/dist/server/app-page-render.d.ts +8 -4
- package/dist/server/app-page-render.js +15 -4
- package/dist/server/app-page-render.js.map +1 -1
- package/dist/server/app-page-request.d.ts +52 -4
- package/dist/server/app-page-request.js +86 -16
- package/dist/server/app-page-request.js.map +1 -1
- package/dist/server/app-page-response.d.ts +4 -11
- package/dist/server/app-page-response.js +7 -19
- package/dist/server/app-page-response.js.map +1 -1
- package/dist/server/app-page-route-wiring.d.ts +22 -8
- package/dist/server/app-page-route-wiring.js +219 -81
- package/dist/server/app-page-route-wiring.js.map +1 -1
- package/dist/server/app-page-stream.d.ts +4 -1
- package/dist/server/app-page-stream.js +2 -1
- package/dist/server/app-page-stream.js.map +1 -1
- package/dist/server/app-render-dependency.d.ts +13 -0
- package/dist/server/app-render-dependency.js +35 -0
- package/dist/server/app-render-dependency.js.map +1 -0
- package/dist/server/app-route-handler-execution.d.ts +1 -0
- package/dist/server/app-route-handler-execution.js +1 -0
- package/dist/server/app-route-handler-execution.js.map +1 -1
- package/dist/server/app-route-handler-response.js +2 -1
- package/dist/server/app-route-handler-response.js.map +1 -1
- package/dist/server/app-route-handler-runtime.d.ts +1 -0
- package/dist/server/app-route-handler-runtime.js +26 -1
- package/dist/server/app-route-handler-runtime.js.map +1 -1
- package/dist/server/app-ssr-entry.d.ts +3 -1
- package/dist/server/app-ssr-entry.js +23 -19
- package/dist/server/app-ssr-entry.js.map +1 -1
- package/dist/server/app-ssr-stream.d.ts +1 -1
- package/dist/server/app-ssr-stream.js +4 -4
- package/dist/server/app-ssr-stream.js.map +1 -1
- package/dist/server/csp.d.ts +12 -0
- package/dist/server/csp.js +46 -0
- package/dist/server/csp.js.map +1 -0
- package/dist/server/dev-server.js +22 -18
- package/dist/server/dev-server.js.map +1 -1
- package/dist/server/html.d.ts +4 -1
- package/dist/server/html.js +11 -1
- package/dist/server/html.js.map +1 -1
- package/dist/server/middleware-response-headers.d.ts +12 -0
- package/dist/server/middleware-response-headers.js +23 -0
- package/dist/server/middleware-response-headers.js.map +1 -0
- package/dist/server/middleware.js +1 -5
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/pages-page-data.d.ts +1 -0
- package/dist/server/pages-page-data.js +2 -2
- package/dist/server/pages-page-data.js.map +1 -1
- package/dist/server/pages-page-response.d.ts +2 -1
- package/dist/server/pages-page-response.js +16 -14
- package/dist/server/pages-page-response.js.map +1 -1
- package/dist/server/prod-server.d.ts +3 -3
- package/dist/server/prod-server.js +5 -4
- package/dist/server/prod-server.js.map +1 -1
- package/dist/server/request-pipeline.d.ts +15 -1
- package/dist/server/request-pipeline.js +88 -5
- package/dist/server/request-pipeline.js.map +1 -1
- package/dist/shims/cache-runtime.d.ts +1 -0
- package/dist/shims/cache-runtime.js +0 -5
- package/dist/shims/cache-runtime.js.map +1 -1
- package/dist/shims/cache.d.ts +1 -0
- package/dist/shims/cache.js +1 -8
- package/dist/shims/cache.js.map +1 -1
- package/dist/shims/client-hook-error.d.ts +14 -0
- package/dist/shims/client-hook-error.js +19 -0
- package/dist/shims/client-hook-error.js.map +1 -0
- package/dist/shims/constants.d.ts +3 -3
- package/dist/shims/constants.js +3 -3
- package/dist/shims/constants.js.map +1 -1
- package/dist/shims/document.d.ts +6 -6
- package/dist/shims/error-boundary.d.ts +4 -4
- package/dist/shims/error-boundary.js +1 -1
- package/dist/shims/error-boundary.js.map +1 -1
- package/dist/shims/form.d.ts +3 -3
- package/dist/shims/head-state.d.ts +1 -0
- package/dist/shims/head-state.js +0 -5
- package/dist/shims/head-state.js.map +1 -1
- package/dist/shims/headers.d.ts +11 -0
- package/dist/shims/headers.js +13 -10
- package/dist/shims/headers.js.map +1 -1
- package/dist/shims/i18n-state.d.ts +1 -0
- package/dist/shims/i18n-state.js +0 -4
- package/dist/shims/i18n-state.js.map +1 -1
- package/dist/shims/internal/app-router-context.d.ts +6 -6
- package/dist/shims/internal/router-context.d.ts +2 -2
- package/dist/shims/layout-segment-context.d.ts +2 -2
- package/dist/shims/link.js +19 -11
- package/dist/shims/link.js.map +1 -1
- package/dist/shims/metadata.d.ts +3 -3
- package/dist/shims/navigation-state.d.ts +2 -0
- package/dist/shims/navigation-state.js +0 -13
- package/dist/shims/navigation-state.js.map +1 -1
- package/dist/shims/navigation.d.ts +55 -8
- package/dist/shims/navigation.js +97 -23
- package/dist/shims/navigation.js.map +1 -1
- package/dist/shims/navigation.react-server.d.ts +14 -0
- package/dist/shims/navigation.react-server.js +29 -0
- package/dist/shims/navigation.react-server.js.map +1 -0
- package/dist/shims/request-context.d.ts +1 -0
- package/dist/shims/request-context.js +0 -9
- package/dist/shims/request-context.js.map +1 -1
- package/dist/shims/request-state-types.d.ts +1 -1
- package/dist/shims/router-state.d.ts +1 -0
- package/dist/shims/router-state.js +0 -5
- package/dist/shims/router-state.js.map +1 -1
- package/dist/shims/script-nonce-context.d.ts +12 -0
- package/dist/shims/script-nonce-context.js +17 -0
- package/dist/shims/script-nonce-context.js.map +1 -0
- package/dist/shims/script.js +41 -10
- package/dist/shims/script.js.map +1 -1
- package/dist/shims/server.js +6 -1
- package/dist/shims/server.js.map +1 -1
- package/dist/shims/slot.d.ts +11 -7
- package/dist/shims/slot.js +28 -19
- package/dist/shims/slot.js.map +1 -1
- package/dist/shims/unified-request-context.d.ts +2 -0
- package/dist/shims/unified-request-context.js +0 -14
- package/dist/shims/unified-request-context.js.map +1 -1
- package/dist/shims/url-safety.js +25 -4
- package/dist/shims/url-safety.js.map +1 -1
- package/dist/utils/mdx-scan.d.ts +10 -0
- package/dist/utils/mdx-scan.js +36 -0
- package/dist/utils/mdx-scan.js.map +1 -0
- package/dist/utils/public-routes.d.ts +5 -0
- package/dist/utils/public-routes.js +50 -0
- package/dist/utils/public-routes.js.map +1 -0
- package/package.json +9 -9
- package/dist/plugins/fix-use-server-closure-collision.d.ts +0 -29
- package/dist/plugins/fix-use-server-closure-collision.js +0 -204
- package/dist/plugins/fix-use-server-closure-collision.js.map +0 -1
package/dist/shims/link.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { resolveRelativeHref, toBrowserNavigationHref, toSameOriginAppPath, withBasePath } from "./url-utils.js";
|
|
3
3
|
import { addLocalePrefix, getDomainLocaleUrl } from "../utils/domain-locale.js";
|
|
4
4
|
import { appendSearchParamsToUrl, urlQueryToSearchParams } from "../utils/query.js";
|
|
5
|
-
import {
|
|
5
|
+
import { createAppPayloadCacheKey } from "../server/app-elements.js";
|
|
6
|
+
import { getCurrentInterceptionContext, getMountedSlotsHeader, getPrefetchedUrls, navigateClientSide, prefetchRscResponse, toRscUrl } from "./navigation.js";
|
|
6
7
|
import { isDangerousScheme } from "./url-safety.js";
|
|
7
8
|
import { getI18nContext } from "./i18n-context.js";
|
|
8
9
|
import React, { createContext, forwardRef, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
@@ -55,17 +56,24 @@ function prefetchUrl(href) {
|
|
|
55
56
|
}
|
|
56
57
|
const fullHref = toBrowserNavigationHref(prefetchHref, window.location.href, __basePath);
|
|
57
58
|
const rscUrl = toRscUrl(fullHref);
|
|
59
|
+
const interceptionContext = getCurrentInterceptionContext();
|
|
60
|
+
const cacheKey = createAppPayloadCacheKey(rscUrl, interceptionContext);
|
|
58
61
|
const prefetched = getPrefetchedUrls();
|
|
59
|
-
if (prefetched.has(
|
|
60
|
-
prefetched.add(
|
|
62
|
+
if (prefetched.has(cacheKey)) return;
|
|
63
|
+
prefetched.add(cacheKey);
|
|
61
64
|
(window.requestIdleCallback ?? ((fn) => setTimeout(fn, 100)))(() => {
|
|
62
|
-
if (typeof window.__VINEXT_RSC_NAVIGATE__ === "function")
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
if (typeof window.__VINEXT_RSC_NAVIGATE__ === "function") {
|
|
66
|
+
const mountedSlotsHeader = getMountedSlotsHeader();
|
|
67
|
+
const headers = new Headers({ Accept: "text/x-component" });
|
|
68
|
+
if (mountedSlotsHeader) headers.set("X-Vinext-Mounted-Slots", mountedSlotsHeader);
|
|
69
|
+
if (interceptionContext !== null) headers.set("X-Vinext-Interception-Context", interceptionContext);
|
|
70
|
+
prefetchRscResponse(rscUrl, fetch(rscUrl, {
|
|
71
|
+
headers,
|
|
72
|
+
credentials: "include",
|
|
73
|
+
priority: "low",
|
|
74
|
+
purpose: "prefetch"
|
|
75
|
+
}), interceptionContext, mountedSlotsHeader);
|
|
76
|
+
} else if (window.__NEXT_DATA__?.__vinext?.pageModuleUrl) {
|
|
69
77
|
const link = document.createElement("link");
|
|
70
78
|
link.rel = "prefetch";
|
|
71
79
|
link.href = fullHref;
|
|
@@ -205,7 +213,7 @@ const Link = forwardRef(function Link({ href, as, replace = false, prefetch: pre
|
|
|
205
213
|
if (mountedRef.current) setPending(false);
|
|
206
214
|
}
|
|
207
215
|
} else try {
|
|
208
|
-
const Router = (await import("next/router")).default;
|
|
216
|
+
const Router = (await import("next/router.js")).default;
|
|
209
217
|
if (replace) await Router.replace(absoluteHref, void 0, { scroll });
|
|
210
218
|
else await Router.push(absoluteHref, void 0, { scroll });
|
|
211
219
|
} catch {
|
package/dist/shims/link.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link.js","names":[],"sources":["../../src/shims/link.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * next/link shim\n *\n * Renders an <a> tag with client-side navigation support.\n * On click, prevents full page reload and triggers client-side\n * page swap via the router's navigation system.\n */\nimport React, {\n forwardRef,\n useRef,\n useEffect,\n useCallback,\n useContext,\n createContext,\n useState,\n type AnchorHTMLAttributes,\n type MouseEvent,\n} from \"react\";\n// Import shared RSC prefetch utilities from navigation shim (relative path\n// so this resolves both via the Vite plugin and in direct vitest imports)\nimport {\n toRscUrl,\n getPrefetchedUrls,\n navigateClientSide,\n prefetchRscResponse,\n} from \"./navigation.js\";\nimport { isDangerousScheme } from \"./url-safety.js\";\nimport {\n resolveRelativeHref,\n toBrowserNavigationHref,\n toSameOriginAppPath,\n withBasePath,\n} from \"./url-utils.js\";\nimport { appendSearchParamsToUrl, type UrlQuery, urlQueryToSearchParams } from \"../utils/query.js\";\nimport { addLocalePrefix, getDomainLocaleUrl, type DomainLocale } from \"../utils/domain-locale.js\";\nimport { getI18nContext } from \"./i18n-context.js\";\nimport type { VinextNextData } from \"../client/vinext-next-data.js\";\n\ntype NavigateEvent = {\n url: URL;\n /** Call to prevent the Link's default navigation (e.g. for View Transitions). */\n preventDefault(): void;\n /** Whether preventDefault() has been called. */\n defaultPrevented: boolean;\n};\n\ntype LinkProps = {\n href: string | { pathname?: string; query?: UrlQuery };\n /** URL displayed in the browser (when href is a route pattern like /user/[id]) */\n as?: string;\n /** Replace the current history entry instead of pushing */\n replace?: boolean;\n /** Prefetch the page in the background (default: true, uses IntersectionObserver) */\n prefetch?: boolean;\n /** Whether to pass the href to the child element */\n passHref?: boolean;\n /** Scroll to top on navigation (default: true) */\n scroll?: boolean;\n /** Locale for i18n (used for locale-prefixed URLs) */\n locale?: string | false;\n /** Called before navigation happens (Next.js 16). Return value is ignored. */\n onNavigate?: (event: NavigateEvent) => void;\n children?: React.ReactNode;\n} & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\">;\n\n// ---------------------------------------------------------------------------\n// useLinkStatus — reports the pending state of a parent <Link> navigation\n// ---------------------------------------------------------------------------\n\ntype LinkStatusContextValue = {\n pending: boolean;\n};\n\nconst LinkStatusContext = createContext<LinkStatusContextValue>({ pending: false });\n\n/**\n * useLinkStatus returns the pending state of the enclosing <Link>.\n * In Next.js, this is used to show loading indicators while a\n * prefetch-triggered navigation is in progress.\n */\nexport function useLinkStatus(): LinkStatusContextValue {\n return useContext(LinkStatusContext);\n}\n\n/** basePath from next.config.js, injected by the plugin at build time */\nconst __basePath: string = process.env.__NEXT_ROUTER_BASEPATH ?? \"\";\n\nfunction resolveHref(href: LinkProps[\"href\"]): string {\n if (typeof href === \"string\") return href;\n let url = href.pathname ?? \"/\";\n if (href.query) {\n const params = urlQueryToSearchParams(href.query);\n url = appendSearchParamsToUrl(url, params);\n }\n return url;\n}\n\n// ---------------------------------------------------------------------------\n// Prefetching infrastructure\n// ---------------------------------------------------------------------------\n\n/**\n * Prefetch a URL for faster navigation.\n *\n * For App Router (RSC): fetches the .rsc payload in the background and\n * stores it in an in-memory cache for instant use during navigation.\n * For Pages Router: injects a <link rel=\"prefetch\"> for the page module.\n *\n * Uses `requestIdleCallback` (or `setTimeout` fallback) to avoid blocking\n * the main thread during initial page load.\n */\nfunction prefetchUrl(href: string): void {\n if (typeof window === \"undefined\") return;\n\n // Normalize same-origin absolute URLs to local paths before prefetching\n let prefetchHref = href;\n if (href.startsWith(\"http://\") || href.startsWith(\"https://\") || href.startsWith(\"//\")) {\n const localPath = toSameOriginAppPath(href, __basePath);\n if (localPath == null) return; // truly external — don't prefetch\n prefetchHref = localPath;\n }\n\n const fullHref = toBrowserNavigationHref(prefetchHref, window.location.href, __basePath);\n\n // Don't prefetch the same URL twice (keyed by rscUrl so the browser\n // entry can clear the key when a cache entry is consumed)\n const rscUrl = toRscUrl(fullHref);\n const prefetched = getPrefetchedUrls();\n if (prefetched.has(rscUrl)) return;\n prefetched.add(rscUrl);\n\n const schedule = window.requestIdleCallback ?? ((fn: () => void) => setTimeout(fn, 100));\n\n schedule(() => {\n if (typeof window.__VINEXT_RSC_NAVIGATE__ === \"function\") {\n prefetchRscResponse(\n rscUrl,\n fetch(rscUrl, {\n headers: { Accept: \"text/x-component\" },\n credentials: \"include\",\n priority: \"low\" as const,\n // @ts-expect-error — purpose is a valid fetch option in some browsers\n purpose: \"prefetch\",\n }),\n );\n } else if ((window.__NEXT_DATA__ as VinextNextData | undefined)?.__vinext?.pageModuleUrl) {\n // Pages Router: inject a prefetch link for the target page module\n // We can't easily resolve the target page's module URL from the Link,\n // so we create a <link rel=\"prefetch\"> for the HTML page which helps\n // the browser's preload scanner.\n const link = document.createElement(\"link\");\n link.rel = \"prefetch\";\n link.href = fullHref;\n link.as = \"document\";\n document.head.appendChild(link);\n }\n });\n}\n\n/**\n * Shared IntersectionObserver for viewport-based prefetching.\n * All Link elements use the same observer to minimize resource usage.\n */\nlet sharedObserver: IntersectionObserver | null = null;\nconst observerCallbacks = new WeakMap<Element, () => void>();\n\nfunction getSharedObserver(): IntersectionObserver | null {\n if (typeof window === \"undefined\" || typeof IntersectionObserver === \"undefined\") return null;\n if (sharedObserver) return sharedObserver;\n\n sharedObserver = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n const callback = observerCallbacks.get(entry.target);\n if (callback) {\n callback();\n // Unobserve after prefetching — only prefetch once\n sharedObserver?.unobserve(entry.target);\n observerCallbacks.delete(entry.target);\n }\n }\n }\n },\n {\n // Start prefetching when the link is within 250px of the viewport.\n // This gives the browser a head start before the user scrolls to it.\n rootMargin: \"250px\",\n },\n );\n\n return sharedObserver;\n}\n\nfunction getDefaultLocale(): string | undefined {\n if (typeof window !== \"undefined\") {\n return window.__VINEXT_DEFAULT_LOCALE__;\n }\n return getI18nContext()?.defaultLocale;\n}\n\nfunction getDomainLocales(): readonly DomainLocale[] | undefined {\n if (typeof window !== \"undefined\") {\n return (window.__NEXT_DATA__ as VinextNextData | undefined)?.domainLocales;\n }\n return getI18nContext()?.domainLocales;\n}\n\nfunction getCurrentHostname(): string | undefined {\n if (typeof window !== \"undefined\") return window.location.hostname;\n return getI18nContext()?.hostname;\n}\n\nfunction getDomainLocaleHref(href: string, locale: string): string | undefined {\n // Only cross-domain locale switches need a special absolute URL here.\n // Same-domain cases fall back to the standard locale-prefix logic below.\n return getDomainLocaleUrl(href, locale, {\n basePath: __basePath,\n currentHostname: getCurrentHostname(),\n domainItems: getDomainLocales(),\n });\n}\n\n/**\n * Apply locale prefix to a URL path based on the locale prop.\n * - locale=\"fr\" → prepend /fr (unless it already has a locale prefix)\n * - locale={false} → use the href as-is (no locale prefix, link to default)\n * - locale=undefined → use current locale (href as-is in most cases)\n */\nfunction applyLocaleToHref(href: string, locale: string | false | undefined): string {\n if (locale === false) {\n // Explicit false: no locale prefix\n return href;\n }\n\n if (locale === undefined) {\n // No locale prop: keep current behavior (href as-is)\n return href;\n }\n\n // Absolute and protocol-relative URLs must not be prefixed — locale\n // only applies to local paths.\n if (href.startsWith(\"http://\") || href.startsWith(\"https://\") || href.startsWith(\"//\")) {\n return href;\n }\n\n const domainLocaleHref = getDomainLocaleHref(href, locale);\n if (domainLocaleHref) {\n return domainLocaleHref;\n }\n\n return addLocalePrefix(href, locale, getDefaultLocale() ?? \"\");\n}\n\nconst Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(\n {\n href,\n as,\n replace = false,\n prefetch: prefetchProp,\n scroll = true,\n children,\n onClick,\n onNavigate,\n ...rest\n },\n forwardedRef,\n) {\n // Extract locale from rest props\n const { locale, ...restWithoutLocale } = rest;\n\n // If `as` is provided, use it as the actual URL (legacy Next.js pattern\n // where href is a route pattern like \"/user/[id]\" and as is \"/user/1\")\n const resolvedHref = as ?? resolveHref(href);\n\n const isDangerous = typeof resolvedHref === \"string\" && isDangerousScheme(resolvedHref);\n\n // Apply locale prefix if specified (safe even for dangerous hrefs since we\n // won't use the result when isDangerous is true)\n const localizedHref = applyLocaleToHref(isDangerous ? \"/\" : resolvedHref, locale);\n // Full href with basePath for browser URLs and fetches\n const fullHref = withBasePath(localizedHref, __basePath);\n\n // Track pending state for useLinkStatus()\n const [pending, setPending] = useState(false);\n const mountedRef = useRef(true);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n // Prefetching: observe the element when it enters the viewport.\n // prefetch={false} disables, prefetch={true} or undefined/null (default) enables.\n const internalRef = useRef<HTMLAnchorElement | null>(null);\n const shouldPrefetch = prefetchProp !== false && !isDangerous;\n\n const setRefs = useCallback(\n (node: HTMLAnchorElement | null) => {\n internalRef.current = node;\n if (typeof forwardedRef === \"function\") forwardedRef(node);\n else if (forwardedRef)\n (forwardedRef as React.MutableRefObject<HTMLAnchorElement | null>).current = node;\n },\n [forwardedRef],\n );\n\n useEffect(() => {\n if (!shouldPrefetch || typeof window === \"undefined\") return;\n const node = internalRef.current;\n if (!node) return;\n\n // Normalize same-origin absolute URLs; skip truly external ones\n let hrefToPrefetch = localizedHref;\n if (\n localizedHref.startsWith(\"http://\") ||\n localizedHref.startsWith(\"https://\") ||\n localizedHref.startsWith(\"//\")\n ) {\n const localPath = toSameOriginAppPath(localizedHref, __basePath);\n if (localPath == null) return; // truly external\n hrefToPrefetch = localPath;\n }\n\n const observer = getSharedObserver();\n if (!observer) return;\n\n observerCallbacks.set(node, () => prefetchUrl(hrefToPrefetch));\n observer.observe(node);\n\n return () => {\n observer.unobserve(node);\n observerCallbacks.delete(node);\n };\n }, [shouldPrefetch, localizedHref]);\n\n const handleClick = async (e: MouseEvent<HTMLAnchorElement>) => {\n if (onClick) onClick(e);\n if (e.defaultPrevented) return;\n\n // Only intercept left clicks without modifiers (standard link behavior)\n if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return;\n }\n\n // Don't intercept links with target (e.g. target=\"_blank\")\n if (e.currentTarget.target && e.currentTarget.target !== \"_self\") {\n return;\n }\n\n // External links: let the browser handle it.\n // Same-origin absolute URLs (e.g. http://localhost:3000/about) are\n // normalized to local paths so they get client-side navigation.\n let navigateHref = localizedHref;\n if (\n resolvedHref.startsWith(\"http://\") ||\n resolvedHref.startsWith(\"https://\") ||\n resolvedHref.startsWith(\"//\")\n ) {\n const localPath = toSameOriginAppPath(resolvedHref, __basePath);\n if (localPath == null) return; // truly external\n navigateHref = localPath;\n }\n\n e.preventDefault();\n\n // Resolve relative hrefs (#hash, ?query) against the current URL once so\n // onNavigate and the actual navigation target stay in sync.\n const absoluteHref = resolveRelativeHref(navigateHref, window.location.href, __basePath);\n const absoluteFullHref = toBrowserNavigationHref(\n navigateHref,\n window.location.href,\n __basePath,\n );\n\n // Call onNavigate callback if provided (Next.js 16 View Transitions support)\n if (onNavigate) {\n try {\n const navUrl = new URL(absoluteFullHref, window.location.origin);\n let prevented = false;\n const navEvent: NavigateEvent = {\n url: navUrl,\n preventDefault() {\n prevented = true;\n },\n get defaultPrevented() {\n return prevented;\n },\n };\n onNavigate(navEvent);\n // If the callback called preventDefault(), skip Link's default navigation.\n // The callback is responsible for its own navigation (e.g. via View Transitions API).\n if (navEvent.defaultPrevented) {\n return;\n }\n } catch {\n // Ignore URL parsing errors for relative/hash hrefs\n }\n }\n\n // App Router: delegate to navigateClientSide which handles scroll save,\n // hash-only changes, RSC fetch, and two-phase URL commit.\n if (typeof window.__VINEXT_RSC_NAVIGATE__ === \"function\") {\n setPending(true);\n try {\n await navigateClientSide(navigateHref, replace ? \"replace\" : \"push\", scroll);\n } finally {\n if (mountedRef.current) setPending(false);\n }\n } else {\n // Next.js only consumes onRouterTransitionStart in the App Router.\n // Pages Router still executes instrumentation-client side effects\n // during startup, but it does not invoke the named export on navigation.\n // Pages Router: use the Router singleton\n try {\n const routerModule = await import(\"next/router\");\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any -- vinext's Router shim accepts (url, as, options)\n const Router = routerModule.default as any;\n if (replace) {\n await Router.replace(absoluteHref, undefined, { scroll });\n } else {\n await Router.push(absoluteHref, undefined, { scroll });\n }\n } catch {\n // Fallback to hard navigation if router fails\n if (replace) {\n window.history.replaceState({}, \"\", absoluteFullHref);\n } else {\n window.history.pushState({}, \"\", absoluteFullHref);\n }\n window.dispatchEvent(new PopStateEvent(\"popstate\"));\n }\n }\n };\n\n // Remove props that shouldn't be on <a>\n const { passHref: _p, ...anchorProps } = restWithoutLocale;\n\n const linkStatusValue = React.useMemo(() => ({ pending }), [pending]);\n\n // Block dangerous URI schemes (javascript:, data:, vbscript:).\n // Render an inert <a> without href to prevent XSS while preserving\n // styling and attributes like className, id, aria-*.\n // This check is placed after all hooks to satisfy the Rules of Hooks.\n if (isDangerous) {\n if (process.env.NODE_ENV !== \"production\") {\n console.warn(`<Link> blocked dangerous href: ${resolvedHref}`);\n }\n return <a {...anchorProps}>{children}</a>;\n }\n\n return (\n <LinkStatusContext.Provider value={linkStatusValue}>\n <a ref={setRefs} href={fullHref} onClick={handleClick} {...anchorProps}>\n {children}\n </a>\n </LinkStatusContext.Provider>\n );\n});\n\nexport default Link;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"link.js","names":[],"sources":["../../src/shims/link.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * next/link shim\n *\n * Renders an <a> tag with client-side navigation support.\n * On click, prevents full page reload and triggers client-side\n * page swap via the router's navigation system.\n */\nimport React, {\n forwardRef,\n useRef,\n useEffect,\n useCallback,\n useContext,\n createContext,\n useState,\n type AnchorHTMLAttributes,\n type MouseEvent,\n} from \"react\";\n// Import shared RSC prefetch utilities from navigation shim (relative path\n// so this resolves both via the Vite plugin and in direct vitest imports)\nimport {\n getCurrentInterceptionContext,\n toRscUrl,\n getPrefetchedUrls,\n getMountedSlotsHeader,\n navigateClientSide,\n prefetchRscResponse,\n} from \"./navigation.js\";\nimport { createAppPayloadCacheKey } from \"../server/app-elements.js\";\nimport { isDangerousScheme } from \"./url-safety.js\";\nimport {\n resolveRelativeHref,\n toBrowserNavigationHref,\n toSameOriginAppPath,\n withBasePath,\n} from \"./url-utils.js\";\nimport { appendSearchParamsToUrl, type UrlQuery, urlQueryToSearchParams } from \"../utils/query.js\";\nimport { addLocalePrefix, getDomainLocaleUrl, type DomainLocale } from \"../utils/domain-locale.js\";\nimport { getI18nContext } from \"./i18n-context.js\";\nimport type { VinextNextData } from \"../client/vinext-next-data.js\";\n\ntype NavigateEvent = {\n url: URL;\n /** Call to prevent the Link's default navigation (e.g. for View Transitions). */\n preventDefault(): void;\n /** Whether preventDefault() has been called. */\n defaultPrevented: boolean;\n};\n\ntype LinkProps = {\n href: string | { pathname?: string; query?: UrlQuery };\n /** URL displayed in the browser (when href is a route pattern like /user/[id]) */\n as?: string;\n /** Replace the current history entry instead of pushing */\n replace?: boolean;\n /** Prefetch the page in the background (default: true, uses IntersectionObserver) */\n prefetch?: boolean;\n /** Whether to pass the href to the child element */\n passHref?: boolean;\n /** Scroll to top on navigation (default: true) */\n scroll?: boolean;\n /** Locale for i18n (used for locale-prefixed URLs) */\n locale?: string | false;\n /** Called before navigation happens (Next.js 16). Return value is ignored. */\n onNavigate?: (event: NavigateEvent) => void;\n children?: React.ReactNode;\n} & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\">;\n\n// ---------------------------------------------------------------------------\n// useLinkStatus — reports the pending state of a parent <Link> navigation\n// ---------------------------------------------------------------------------\n\ntype LinkStatusContextValue = {\n pending: boolean;\n};\n\nconst LinkStatusContext = createContext<LinkStatusContextValue>({ pending: false });\n\n/**\n * useLinkStatus returns the pending state of the enclosing <Link>.\n * In Next.js, this is used to show loading indicators while a\n * prefetch-triggered navigation is in progress.\n */\nexport function useLinkStatus(): LinkStatusContextValue {\n return useContext(LinkStatusContext);\n}\n\n/** basePath from next.config.js, injected by the plugin at build time */\nconst __basePath: string = process.env.__NEXT_ROUTER_BASEPATH ?? \"\";\n\nfunction resolveHref(href: LinkProps[\"href\"]): string {\n if (typeof href === \"string\") return href;\n let url = href.pathname ?? \"/\";\n if (href.query) {\n const params = urlQueryToSearchParams(href.query);\n url = appendSearchParamsToUrl(url, params);\n }\n return url;\n}\n\n// ---------------------------------------------------------------------------\n// Prefetching infrastructure\n// ---------------------------------------------------------------------------\n\n/**\n * Prefetch a URL for faster navigation.\n *\n * For App Router (RSC): fetches the .rsc payload in the background and\n * stores it in an in-memory cache for instant use during navigation.\n * For Pages Router: injects a <link rel=\"prefetch\"> for the page module.\n *\n * Uses `requestIdleCallback` (or `setTimeout` fallback) to avoid blocking\n * the main thread during initial page load.\n */\nfunction prefetchUrl(href: string): void {\n if (typeof window === \"undefined\") return;\n\n // Normalize same-origin absolute URLs to local paths before prefetching\n let prefetchHref = href;\n if (href.startsWith(\"http://\") || href.startsWith(\"https://\") || href.startsWith(\"//\")) {\n const localPath = toSameOriginAppPath(href, __basePath);\n if (localPath == null) return; // truly external — don't prefetch\n prefetchHref = localPath;\n }\n\n const fullHref = toBrowserNavigationHref(prefetchHref, window.location.href, __basePath);\n\n // Distinguish the same visible URL when it is prefetched from different\n // interception sources such as /feed vs /gallery.\n const rscUrl = toRscUrl(fullHref);\n const interceptionContext = getCurrentInterceptionContext();\n const cacheKey = createAppPayloadCacheKey(rscUrl, interceptionContext);\n const prefetched = getPrefetchedUrls();\n if (prefetched.has(cacheKey)) return;\n prefetched.add(cacheKey);\n\n const schedule = window.requestIdleCallback ?? ((fn: () => void) => setTimeout(fn, 100));\n\n schedule(() => {\n if (typeof window.__VINEXT_RSC_NAVIGATE__ === \"function\") {\n const mountedSlotsHeader = getMountedSlotsHeader();\n const headers = new Headers({ Accept: \"text/x-component\" });\n if (mountedSlotsHeader) {\n headers.set(\"X-Vinext-Mounted-Slots\", mountedSlotsHeader);\n }\n if (interceptionContext !== null) {\n headers.set(\"X-Vinext-Interception-Context\", interceptionContext);\n }\n prefetchRscResponse(\n rscUrl,\n fetch(rscUrl, {\n headers,\n credentials: \"include\",\n priority: \"low\" as const,\n // @ts-expect-error — purpose is a valid fetch option in some browsers\n purpose: \"prefetch\",\n }),\n interceptionContext,\n mountedSlotsHeader,\n );\n } else if ((window.__NEXT_DATA__ as VinextNextData | undefined)?.__vinext?.pageModuleUrl) {\n // Pages Router: inject a prefetch link for the target page module\n // We can't easily resolve the target page's module URL from the Link,\n // so we create a <link rel=\"prefetch\"> for the HTML page which helps\n // the browser's preload scanner.\n const link = document.createElement(\"link\");\n link.rel = \"prefetch\";\n link.href = fullHref;\n link.as = \"document\";\n document.head.appendChild(link);\n }\n });\n}\n\n/**\n * Shared IntersectionObserver for viewport-based prefetching.\n * All Link elements use the same observer to minimize resource usage.\n */\nlet sharedObserver: IntersectionObserver | null = null;\nconst observerCallbacks = new WeakMap<Element, () => void>();\n\nfunction getSharedObserver(): IntersectionObserver | null {\n if (typeof window === \"undefined\" || typeof IntersectionObserver === \"undefined\") return null;\n if (sharedObserver) return sharedObserver;\n\n sharedObserver = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n const callback = observerCallbacks.get(entry.target);\n if (callback) {\n callback();\n // Unobserve after prefetching — only prefetch once\n sharedObserver?.unobserve(entry.target);\n observerCallbacks.delete(entry.target);\n }\n }\n }\n },\n {\n // Start prefetching when the link is within 250px of the viewport.\n // This gives the browser a head start before the user scrolls to it.\n rootMargin: \"250px\",\n },\n );\n\n return sharedObserver;\n}\n\nfunction getDefaultLocale(): string | undefined {\n if (typeof window !== \"undefined\") {\n return window.__VINEXT_DEFAULT_LOCALE__;\n }\n return getI18nContext()?.defaultLocale;\n}\n\nfunction getDomainLocales(): readonly DomainLocale[] | undefined {\n if (typeof window !== \"undefined\") {\n return (window.__NEXT_DATA__ as VinextNextData | undefined)?.domainLocales;\n }\n return getI18nContext()?.domainLocales;\n}\n\nfunction getCurrentHostname(): string | undefined {\n if (typeof window !== \"undefined\") return window.location.hostname;\n return getI18nContext()?.hostname;\n}\n\nfunction getDomainLocaleHref(href: string, locale: string): string | undefined {\n // Only cross-domain locale switches need a special absolute URL here.\n // Same-domain cases fall back to the standard locale-prefix logic below.\n return getDomainLocaleUrl(href, locale, {\n basePath: __basePath,\n currentHostname: getCurrentHostname(),\n domainItems: getDomainLocales(),\n });\n}\n\n/**\n * Apply locale prefix to a URL path based on the locale prop.\n * - locale=\"fr\" → prepend /fr (unless it already has a locale prefix)\n * - locale={false} → use the href as-is (no locale prefix, link to default)\n * - locale=undefined → use current locale (href as-is in most cases)\n */\nfunction applyLocaleToHref(href: string, locale: string | false | undefined): string {\n if (locale === false) {\n // Explicit false: no locale prefix\n return href;\n }\n\n if (locale === undefined) {\n // No locale prop: keep current behavior (href as-is)\n return href;\n }\n\n // Absolute and protocol-relative URLs must not be prefixed — locale\n // only applies to local paths.\n if (href.startsWith(\"http://\") || href.startsWith(\"https://\") || href.startsWith(\"//\")) {\n return href;\n }\n\n const domainLocaleHref = getDomainLocaleHref(href, locale);\n if (domainLocaleHref) {\n return domainLocaleHref;\n }\n\n return addLocalePrefix(href, locale, getDefaultLocale() ?? \"\");\n}\n\nconst Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(\n {\n href,\n as,\n replace = false,\n prefetch: prefetchProp,\n scroll = true,\n children,\n onClick,\n onNavigate,\n ...rest\n },\n forwardedRef,\n) {\n // Extract locale from rest props\n const { locale, ...restWithoutLocale } = rest;\n\n // If `as` is provided, use it as the actual URL (legacy Next.js pattern\n // where href is a route pattern like \"/user/[id]\" and as is \"/user/1\")\n const resolvedHref = as ?? resolveHref(href);\n\n const isDangerous = typeof resolvedHref === \"string\" && isDangerousScheme(resolvedHref);\n\n // Apply locale prefix if specified (safe even for dangerous hrefs since we\n // won't use the result when isDangerous is true)\n const localizedHref = applyLocaleToHref(isDangerous ? \"/\" : resolvedHref, locale);\n // Full href with basePath for browser URLs and fetches\n const fullHref = withBasePath(localizedHref, __basePath);\n\n // Track pending state for useLinkStatus()\n const [pending, setPending] = useState(false);\n const mountedRef = useRef(true);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n };\n }, []);\n\n // Prefetching: observe the element when it enters the viewport.\n // prefetch={false} disables, prefetch={true} or undefined/null (default) enables.\n const internalRef = useRef<HTMLAnchorElement | null>(null);\n const shouldPrefetch = prefetchProp !== false && !isDangerous;\n\n const setRefs = useCallback(\n (node: HTMLAnchorElement | null) => {\n internalRef.current = node;\n if (typeof forwardedRef === \"function\") forwardedRef(node);\n else if (forwardedRef)\n (forwardedRef as React.MutableRefObject<HTMLAnchorElement | null>).current = node;\n },\n [forwardedRef],\n );\n\n useEffect(() => {\n if (!shouldPrefetch || typeof window === \"undefined\") return;\n const node = internalRef.current;\n if (!node) return;\n\n // Normalize same-origin absolute URLs; skip truly external ones\n let hrefToPrefetch = localizedHref;\n if (\n localizedHref.startsWith(\"http://\") ||\n localizedHref.startsWith(\"https://\") ||\n localizedHref.startsWith(\"//\")\n ) {\n const localPath = toSameOriginAppPath(localizedHref, __basePath);\n if (localPath == null) return; // truly external\n hrefToPrefetch = localPath;\n }\n\n const observer = getSharedObserver();\n if (!observer) return;\n\n observerCallbacks.set(node, () => prefetchUrl(hrefToPrefetch));\n observer.observe(node);\n\n return () => {\n observer.unobserve(node);\n observerCallbacks.delete(node);\n };\n }, [shouldPrefetch, localizedHref]);\n\n const handleClick = async (e: MouseEvent<HTMLAnchorElement>) => {\n if (onClick) onClick(e);\n if (e.defaultPrevented) return;\n\n // Only intercept left clicks without modifiers (standard link behavior)\n if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return;\n }\n\n // Don't intercept links with target (e.g. target=\"_blank\")\n if (e.currentTarget.target && e.currentTarget.target !== \"_self\") {\n return;\n }\n\n // External links: let the browser handle it.\n // Same-origin absolute URLs (e.g. http://localhost:3000/about) are\n // normalized to local paths so they get client-side navigation.\n let navigateHref = localizedHref;\n if (\n resolvedHref.startsWith(\"http://\") ||\n resolvedHref.startsWith(\"https://\") ||\n resolvedHref.startsWith(\"//\")\n ) {\n const localPath = toSameOriginAppPath(resolvedHref, __basePath);\n if (localPath == null) return; // truly external\n navigateHref = localPath;\n }\n\n e.preventDefault();\n\n // Resolve relative hrefs (#hash, ?query) against the current URL once so\n // onNavigate and the actual navigation target stay in sync.\n const absoluteHref = resolveRelativeHref(navigateHref, window.location.href, __basePath);\n const absoluteFullHref = toBrowserNavigationHref(\n navigateHref,\n window.location.href,\n __basePath,\n );\n\n // Call onNavigate callback if provided (Next.js 16 View Transitions support)\n if (onNavigate) {\n try {\n const navUrl = new URL(absoluteFullHref, window.location.origin);\n let prevented = false;\n const navEvent: NavigateEvent = {\n url: navUrl,\n preventDefault() {\n prevented = true;\n },\n get defaultPrevented() {\n return prevented;\n },\n };\n onNavigate(navEvent);\n // If the callback called preventDefault(), skip Link's default navigation.\n // The callback is responsible for its own navigation (e.g. via View Transitions API).\n if (navEvent.defaultPrevented) {\n return;\n }\n } catch {\n // Ignore URL parsing errors for relative/hash hrefs\n }\n }\n\n // App Router: delegate to navigateClientSide which handles scroll save,\n // hash-only changes, RSC fetch, and two-phase URL commit.\n if (typeof window.__VINEXT_RSC_NAVIGATE__ === \"function\") {\n setPending(true);\n try {\n await navigateClientSide(navigateHref, replace ? \"replace\" : \"push\", scroll);\n } finally {\n if (mountedRef.current) setPending(false);\n }\n } else {\n // Next.js only consumes onRouterTransitionStart in the App Router.\n // Pages Router still executes instrumentation-client side effects\n // during startup, but it does not invoke the named export on navigation.\n // Pages Router: use the Router singleton\n try {\n const routerModule = await import(\"next/router\");\n // oxlint-disable-next-line @typescript-eslint/no-explicit-any -- vinext's Router shim accepts (url, as, options)\n const Router = routerModule.default as any;\n if (replace) {\n await Router.replace(absoluteHref, undefined, { scroll });\n } else {\n await Router.push(absoluteHref, undefined, { scroll });\n }\n } catch {\n // Fallback to hard navigation if router fails\n if (replace) {\n window.history.replaceState({}, \"\", absoluteFullHref);\n } else {\n window.history.pushState({}, \"\", absoluteFullHref);\n }\n window.dispatchEvent(new PopStateEvent(\"popstate\"));\n }\n }\n };\n\n // Remove props that shouldn't be on <a>\n const { passHref: _p, ...anchorProps } = restWithoutLocale;\n\n const linkStatusValue = React.useMemo(() => ({ pending }), [pending]);\n\n // Block dangerous URI schemes (javascript:, data:, vbscript:).\n // Render an inert <a> without href to prevent XSS while preserving\n // styling and attributes like className, id, aria-*.\n // This check is placed after all hooks to satisfy the Rules of Hooks.\n if (isDangerous) {\n if (process.env.NODE_ENV !== \"production\") {\n console.warn(`<Link> blocked dangerous href: ${resolvedHref}`);\n }\n return <a {...anchorProps}>{children}</a>;\n }\n\n return (\n <LinkStatusContext.Provider value={linkStatusValue}>\n <a ref={setRefs} href={fullHref} onClick={handleClick} {...anchorProps}>\n {children}\n </a>\n </LinkStatusContext.Provider>\n );\n});\n\nexport default Link;\n"],"mappings":"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"}
|
package/dist/shims/metadata.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
1
|
+
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
2
|
|
|
3
3
|
//#region src/shims/metadata.d.ts
|
|
4
4
|
type Viewport = {
|
|
@@ -32,7 +32,7 @@ declare function ViewportHead({
|
|
|
32
32
|
viewport
|
|
33
33
|
}: {
|
|
34
34
|
viewport: Viewport;
|
|
35
|
-
}): react_jsx_runtime0.JSX.Element;
|
|
35
|
+
}): _$react_jsx_runtime0.JSX.Element;
|
|
36
36
|
type Metadata = {
|
|
37
37
|
title?: string | {
|
|
38
38
|
default?: string;
|
|
@@ -251,7 +251,7 @@ declare function MetadataHead({
|
|
|
251
251
|
metadata
|
|
252
252
|
}: {
|
|
253
253
|
metadata: Metadata;
|
|
254
|
-
}): react_jsx_runtime0.JSX.Element;
|
|
254
|
+
}): _$react_jsx_runtime0.JSX.Element;
|
|
255
255
|
//#endregion
|
|
256
256
|
export { DEFAULT_VIEWPORT, Metadata, MetadataHead, Viewport, ViewportHead, mergeMetadata, mergeViewport, resolveModuleMetadata, resolveModuleViewport };
|
|
257
257
|
//# sourceMappingURL=metadata.d.ts.map
|
|
@@ -10,6 +10,7 @@ type NavigationState = {
|
|
|
10
10
|
* Ensures per-request isolation for navigation context and
|
|
11
11
|
* useServerInsertedHTML callbacks on concurrent runtimes.
|
|
12
12
|
*/
|
|
13
|
+
declare function runWithNavigationContext<T>(fn: () => Promise<T>): Promise<T>;
|
|
13
14
|
declare function runWithNavigationContext<T>(fn: () => T | Promise<T>): T | Promise<T>;
|
|
14
15
|
/**
|
|
15
16
|
* Run a function with a fresh useServerInsertedHTML callback list while
|
|
@@ -19,6 +20,7 @@ declare function runWithNavigationContext<T>(fn: () => T | Promise<T>): T | Prom
|
|
|
19
20
|
* but it needs a fresh callback collection so CSS-in-JS insertions from the
|
|
20
21
|
* streamed render cannot accumulate into the cache-fill render.
|
|
21
22
|
*/
|
|
23
|
+
declare function runWithServerInsertedHTMLState<T>(fn: () => Promise<T>): Promise<T>;
|
|
22
24
|
declare function runWithServerInsertedHTMLState<T>(fn: () => T | Promise<T>): T | Promise<T>;
|
|
23
25
|
//#endregion
|
|
24
26
|
export { NavigationState, runWithNavigationContext, runWithServerInsertedHTMLState };
|
|
@@ -26,11 +26,6 @@ function _getState() {
|
|
|
26
26
|
if (isInsideUnifiedScope()) return getRequestContext();
|
|
27
27
|
return _als.getStore() ?? _fallbackState;
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
30
|
-
* Run a function within a navigation ALS scope.
|
|
31
|
-
* Ensures per-request isolation for navigation context and
|
|
32
|
-
* useServerInsertedHTML callbacks on concurrent runtimes.
|
|
33
|
-
*/
|
|
34
29
|
function runWithNavigationContext(fn) {
|
|
35
30
|
if (isInsideUnifiedScope()) return runWithUnifiedStateMutation((uCtx) => {
|
|
36
31
|
uCtx.serverContext = null;
|
|
@@ -41,14 +36,6 @@ function runWithNavigationContext(fn) {
|
|
|
41
36
|
serverInsertedHTMLCallbacks: []
|
|
42
37
|
}, fn);
|
|
43
38
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Run a function with a fresh useServerInsertedHTML callback list while
|
|
46
|
-
* preserving the current navigation context.
|
|
47
|
-
*
|
|
48
|
-
* Used by the Pages Router ISR cache-fill pass: it is the same request/path,
|
|
49
|
-
* but it needs a fresh callback collection so CSS-in-JS insertions from the
|
|
50
|
-
* streamed render cannot accumulate into the cache-fill render.
|
|
51
|
-
*/
|
|
52
39
|
function runWithServerInsertedHTMLState(fn) {
|
|
53
40
|
if (isInsideUnifiedScope()) return runWithUnifiedStateMutation((uCtx) => {
|
|
54
41
|
uCtx.serverInsertedHTMLCallbacks = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation-state.js","names":[],"sources":["../../src/shims/navigation-state.ts"],"sourcesContent":["/**\n * Server-only navigation state backed by AsyncLocalStorage.\n *\n * This module provides request-scoped isolation for navigation context\n * and useServerInsertedHTML callbacks. Without ALS, concurrent requests\n * on Cloudflare Workers would share module-level state and leak data\n * (pathnames, params, CSS-in-JS styles) between requests.\n *\n * This module is server-only — it imports node:async_hooks and must NOT\n * be bundled for the browser. The dual-environment navigation.ts shim\n * uses a registration pattern so it works in both environments.\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport {\n _registerStateAccessors,\n type NavigationContext,\n GLOBAL_ACCESSORS_KEY,\n} from \"./navigation.js\";\nimport {\n isInsideUnifiedScope,\n getRequestContext,\n runWithUnifiedStateMutation,\n} from \"./unified-request-context.js\";\n\n// ---------------------------------------------------------------------------\n// ALS setup — same pattern as headers.ts\n// ---------------------------------------------------------------------------\n\nexport type NavigationState = {\n serverContext: NavigationContext | null;\n serverInsertedHTMLCallbacks: Array<() => unknown>;\n};\n\nconst _ALS_KEY = Symbol.for(\"vinext.navigation.als\");\nconst _FALLBACK_KEY = Symbol.for(\"vinext.navigation.fallback\");\nconst _g = globalThis as unknown as Record<PropertyKey, unknown>;\nconst _als = (_g[_ALS_KEY] ??=\n new AsyncLocalStorage<NavigationState>()) as AsyncLocalStorage<NavigationState>;\n\nconst _fallbackState = (_g[_FALLBACK_KEY] ??= {\n serverContext: null,\n serverInsertedHTMLCallbacks: [],\n} satisfies NavigationState) as NavigationState;\n\nfunction _getState(): NavigationState {\n if (isInsideUnifiedScope()) {\n return getRequestContext();\n }\n return _als.getStore() ?? _fallbackState;\n}\n\n/**\n * Run a function within a navigation ALS scope.\n * Ensures per-request isolation for navigation context and\n * useServerInsertedHTML callbacks on concurrent runtimes.\n */\nexport function runWithNavigationContext<T>(fn: () => T | Promise<T>): T | Promise<T> {\n if (isInsideUnifiedScope()) {\n return runWithUnifiedStateMutation((uCtx) => {\n uCtx.serverContext = null;\n uCtx.serverInsertedHTMLCallbacks = [];\n }, fn);\n }\n const state: NavigationState = {\n serverContext: null,\n serverInsertedHTMLCallbacks: [],\n };\n return _als.run(state, fn);\n}\n\n/**\n * Run a function with a fresh useServerInsertedHTML callback list while\n * preserving the current navigation context.\n *\n * Used by the Pages Router ISR cache-fill pass: it is the same request/path,\n * but it needs a fresh callback collection so CSS-in-JS insertions from the\n * streamed render cannot accumulate into the cache-fill render.\n */\nexport function runWithServerInsertedHTMLState<T>(fn: () => T | Promise<T>): T | Promise<T> {\n if (isInsideUnifiedScope()) {\n return runWithUnifiedStateMutation((uCtx) => {\n uCtx.serverInsertedHTMLCallbacks = [];\n }, fn);\n }\n\n const parentState = _als.getStore() ?? _fallbackState;\n const state: NavigationState = {\n serverContext: parentState.serverContext,\n serverInsertedHTMLCallbacks: [],\n };\n return _als.run(state, fn);\n}\n\n// ---------------------------------------------------------------------------\n// Register ALS-backed accessors into navigation.ts\n//\n// Two registration paths (issue #688):\n// 1. _registerStateAccessors — updates the module-level function pointers\n// in the same module instance that imported us (the SSR entry's copy).\n// 2. globalThis[Symbol.for(...)] — makes the accessors discoverable by ANY\n// module instance of navigation.ts, even if Vite created a separate one\n// for \"use client\" components due to pre-bundling or env separation.\n// ---------------------------------------------------------------------------\n\nconst _accessors = {\n getServerContext(): NavigationContext | null {\n return _getState().serverContext;\n },\n\n setServerContext(ctx: NavigationContext | null): void {\n _getState().serverContext = ctx;\n },\n\n getInsertedHTMLCallbacks(): Array<() => unknown> {\n return _getState().serverInsertedHTMLCallbacks;\n },\n\n clearInsertedHTMLCallbacks(): void {\n _getState().serverInsertedHTMLCallbacks = [];\n },\n} satisfies Parameters<typeof _registerStateAccessors>[0];\n\n_registerStateAccessors(_accessors);\n(globalThis as unknown as Record<PropertyKey, unknown>)[GLOBAL_ACCESSORS_KEY] = _accessors;\n"],"mappings":";;;;;;;;;;;;;;;;AAkCA,MAAM,WAAW,OAAO,IAAI,wBAAwB;AACpD,MAAM,gBAAgB,OAAO,IAAI,6BAA6B;AAC9D,MAAM,KAAK;AACX,MAAM,OAAQ,GAAG,cACf,IAAI,mBAAoC;AAE1C,MAAM,iBAAkB,GAAG,mBAAmB;CAC5C,eAAe;CACf,6BAA6B,EAAE;CAChC;AAED,SAAS,YAA6B;AACpC,KAAI,sBAAsB,CACxB,QAAO,mBAAmB;AAE5B,QAAO,KAAK,UAAU,IAAI
|
|
1
|
+
{"version":3,"file":"navigation-state.js","names":[],"sources":["../../src/shims/navigation-state.ts"],"sourcesContent":["/**\n * Server-only navigation state backed by AsyncLocalStorage.\n *\n * This module provides request-scoped isolation for navigation context\n * and useServerInsertedHTML callbacks. Without ALS, concurrent requests\n * on Cloudflare Workers would share module-level state and leak data\n * (pathnames, params, CSS-in-JS styles) between requests.\n *\n * This module is server-only — it imports node:async_hooks and must NOT\n * be bundled for the browser. The dual-environment navigation.ts shim\n * uses a registration pattern so it works in both environments.\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport {\n _registerStateAccessors,\n type NavigationContext,\n GLOBAL_ACCESSORS_KEY,\n} from \"./navigation.js\";\nimport {\n isInsideUnifiedScope,\n getRequestContext,\n runWithUnifiedStateMutation,\n} from \"./unified-request-context.js\";\n\n// ---------------------------------------------------------------------------\n// ALS setup — same pattern as headers.ts\n// ---------------------------------------------------------------------------\n\nexport type NavigationState = {\n serverContext: NavigationContext | null;\n serverInsertedHTMLCallbacks: Array<() => unknown>;\n};\n\nconst _ALS_KEY = Symbol.for(\"vinext.navigation.als\");\nconst _FALLBACK_KEY = Symbol.for(\"vinext.navigation.fallback\");\nconst _g = globalThis as unknown as Record<PropertyKey, unknown>;\nconst _als = (_g[_ALS_KEY] ??=\n new AsyncLocalStorage<NavigationState>()) as AsyncLocalStorage<NavigationState>;\n\nconst _fallbackState = (_g[_FALLBACK_KEY] ??= {\n serverContext: null,\n serverInsertedHTMLCallbacks: [],\n} satisfies NavigationState) as NavigationState;\n\nfunction _getState(): NavigationState {\n if (isInsideUnifiedScope()) {\n return getRequestContext();\n }\n return _als.getStore() ?? _fallbackState;\n}\n\n/**\n * Run a function within a navigation ALS scope.\n * Ensures per-request isolation for navigation context and\n * useServerInsertedHTML callbacks on concurrent runtimes.\n */\nexport function runWithNavigationContext<T>(fn: () => Promise<T>): Promise<T>;\nexport function runWithNavigationContext<T>(fn: () => T | Promise<T>): T | Promise<T>;\nexport function runWithNavigationContext<T>(fn: () => T | Promise<T>): T | Promise<T> {\n if (isInsideUnifiedScope()) {\n return runWithUnifiedStateMutation((uCtx) => {\n uCtx.serverContext = null;\n uCtx.serverInsertedHTMLCallbacks = [];\n }, fn);\n }\n const state: NavigationState = {\n serverContext: null,\n serverInsertedHTMLCallbacks: [],\n };\n return _als.run(state, fn);\n}\n\n/**\n * Run a function with a fresh useServerInsertedHTML callback list while\n * preserving the current navigation context.\n *\n * Used by the Pages Router ISR cache-fill pass: it is the same request/path,\n * but it needs a fresh callback collection so CSS-in-JS insertions from the\n * streamed render cannot accumulate into the cache-fill render.\n */\nexport function runWithServerInsertedHTMLState<T>(fn: () => Promise<T>): Promise<T>;\nexport function runWithServerInsertedHTMLState<T>(fn: () => T | Promise<T>): T | Promise<T>;\nexport function runWithServerInsertedHTMLState<T>(fn: () => T | Promise<T>): T | Promise<T> {\n if (isInsideUnifiedScope()) {\n return runWithUnifiedStateMutation((uCtx) => {\n uCtx.serverInsertedHTMLCallbacks = [];\n }, fn);\n }\n\n const parentState = _als.getStore() ?? _fallbackState;\n const state: NavigationState = {\n serverContext: parentState.serverContext,\n serverInsertedHTMLCallbacks: [],\n };\n return _als.run(state, fn);\n}\n\n// ---------------------------------------------------------------------------\n// Register ALS-backed accessors into navigation.ts\n//\n// Two registration paths (issue #688):\n// 1. _registerStateAccessors — updates the module-level function pointers\n// in the same module instance that imported us (the SSR entry's copy).\n// 2. globalThis[Symbol.for(...)] — makes the accessors discoverable by ANY\n// module instance of navigation.ts, even if Vite created a separate one\n// for \"use client\" components due to pre-bundling or env separation.\n// ---------------------------------------------------------------------------\n\nconst _accessors = {\n getServerContext(): NavigationContext | null {\n return _getState().serverContext;\n },\n\n setServerContext(ctx: NavigationContext | null): void {\n _getState().serverContext = ctx;\n },\n\n getInsertedHTMLCallbacks(): Array<() => unknown> {\n return _getState().serverInsertedHTMLCallbacks;\n },\n\n clearInsertedHTMLCallbacks(): void {\n _getState().serverInsertedHTMLCallbacks = [];\n },\n} satisfies Parameters<typeof _registerStateAccessors>[0];\n\n_registerStateAccessors(_accessors);\n(globalThis as unknown as Record<PropertyKey, unknown>)[GLOBAL_ACCESSORS_KEY] = _accessors;\n"],"mappings":";;;;;;;;;;;;;;;;AAkCA,MAAM,WAAW,OAAO,IAAI,wBAAwB;AACpD,MAAM,gBAAgB,OAAO,IAAI,6BAA6B;AAC9D,MAAM,KAAK;AACX,MAAM,OAAQ,GAAG,cACf,IAAI,mBAAoC;AAE1C,MAAM,iBAAkB,GAAG,mBAAmB;CAC5C,eAAe;CACf,6BAA6B,EAAE;CAChC;AAED,SAAS,YAA6B;AACpC,KAAI,sBAAsB,CACxB,QAAO,mBAAmB;AAE5B,QAAO,KAAK,UAAU,IAAI;;AAU5B,SAAgB,yBAA4B,IAA0C;AACpF,KAAI,sBAAsB,CACxB,QAAO,6BAA6B,SAAS;AAC3C,OAAK,gBAAgB;AACrB,OAAK,8BAA8B,EAAE;IACpC,GAAG;AAMR,QAAO,KAAK,IAJmB;EAC7B,eAAe;EACf,6BAA6B,EAAE;EAChC,EACsB,GAAG;;AAa5B,SAAgB,+BAAkC,IAA0C;AAC1F,KAAI,sBAAsB,CACxB,QAAO,6BAA6B,SAAS;AAC3C,OAAK,8BAA8B,EAAE;IACpC,GAAG;CAIR,MAAM,QAAyB;EAC7B,gBAFkB,KAAK,UAAU,IAAI,gBAEV;EAC3B,6BAA6B,EAAE;EAChC;AACD,QAAO,KAAK,IAAI,OAAO,GAAG;;AAc5B,MAAM,aAAa;CACjB,mBAA6C;AAC3C,SAAO,WAAW,CAAC;;CAGrB,iBAAiB,KAAqC;AACpD,aAAW,CAAC,gBAAgB;;CAG9B,2BAAiD;AAC/C,SAAO,WAAW,CAAC;;CAGrB,6BAAmC;AACjC,aAAW,CAAC,8BAA8B,EAAE;;CAE/C;AAED,wBAAwB,WAAW;AACnC,WAAwD,wBAAwB"}
|
|
@@ -57,6 +57,7 @@ declare const PREFETCH_CACHE_TTL = 30000;
|
|
|
57
57
|
type CachedRscResponse = {
|
|
58
58
|
buffer: ArrayBuffer;
|
|
59
59
|
contentType: string;
|
|
60
|
+
mountedSlotsHeader?: string | null;
|
|
60
61
|
paramsHeader: string | null;
|
|
61
62
|
url: string;
|
|
62
63
|
};
|
|
@@ -71,11 +72,13 @@ type PrefetchCacheEntry = {
|
|
|
71
72
|
* are consistent regardless of the `trailingSlash` config setting.
|
|
72
73
|
*/
|
|
73
74
|
declare function toRscUrl(href: string): string;
|
|
75
|
+
declare function getCurrentInterceptionContext(): string | null;
|
|
76
|
+
declare function getCurrentNextUrl(): string;
|
|
74
77
|
/** Get or create the shared in-memory RSC prefetch cache on window. */
|
|
75
78
|
declare function getPrefetchCache(): Map<string, PrefetchCacheEntry>;
|
|
76
79
|
/**
|
|
77
80
|
* Get or create the shared set of already-prefetched RSC URLs on window.
|
|
78
|
-
* Keyed by
|
|
81
|
+
* Keyed by interception-aware cache key so distinct source routes do not alias.
|
|
79
82
|
*/
|
|
80
83
|
declare function getPrefetchedUrls(): Set<string>;
|
|
81
84
|
/**
|
|
@@ -85,13 +88,16 @@ declare function getPrefetchedUrls(): Set<string>;
|
|
|
85
88
|
* (the caller falls back to a fresh fetch, which is acceptable).
|
|
86
89
|
*
|
|
87
90
|
* Prefer prefetchRscResponse() for new call-sites — it handles the full
|
|
88
|
-
* prefetch lifecycle including dedup
|
|
89
|
-
* backward compatibility and test
|
|
91
|
+
* prefetch lifecycle including dedup and explicit slot context.
|
|
92
|
+
* storePrefetchResponse() is kept for backward compatibility and test
|
|
93
|
+
* helpers. It is slot-unaware: the snapshot's mountedSlotsHeader comes
|
|
94
|
+
* from the response headers, not the caller, so consumePrefetchResponse
|
|
95
|
+
* may reject the entry if the caller's slot context differs.
|
|
90
96
|
*
|
|
91
97
|
* NB: Caller is responsible for managing getPrefetchedUrls() — this
|
|
92
98
|
* function only stores the response in the prefetch cache.
|
|
93
99
|
*/
|
|
94
|
-
declare function storePrefetchResponse(rscUrl: string, response: Response): void;
|
|
100
|
+
declare function storePrefetchResponse(rscUrl: string, response: Response, interceptionContext?: string | null): void;
|
|
95
101
|
/**
|
|
96
102
|
* Snapshot an RSC response to an ArrayBuffer for caching and replay.
|
|
97
103
|
* Consumes the response body and stores it with content-type and URL metadata.
|
|
@@ -120,13 +126,35 @@ declare function restoreRscResponse(cached: CachedRscResponse, copy?: boolean):
|
|
|
120
126
|
* Enforces a maximum cache size to prevent unbounded memory growth on
|
|
121
127
|
* link-heavy pages.
|
|
122
128
|
*/
|
|
123
|
-
declare function prefetchRscResponse(rscUrl: string, fetchPromise: Promise<Response
|
|
129
|
+
declare function prefetchRscResponse(rscUrl: string, fetchPromise: Promise<Response>, interceptionContext?: string | null, mountedSlotsHeader?: string | null): void;
|
|
124
130
|
/**
|
|
125
131
|
* Consume a prefetched response for a given rscUrl.
|
|
126
132
|
* Only returns settled (non-pending) snapshots synchronously.
|
|
127
133
|
* Returns null if the entry is still in flight or doesn't exist.
|
|
128
134
|
*/
|
|
129
|
-
declare function consumePrefetchResponse(rscUrl: string): CachedRscResponse | null;
|
|
135
|
+
declare function consumePrefetchResponse(rscUrl: string, interceptionContext?: string | null, mountedSlotsHeader?: string | null): CachedRscResponse | null;
|
|
136
|
+
type NavigationListener = () => void;
|
|
137
|
+
type ClientNavigationState = {
|
|
138
|
+
listeners: Set<NavigationListener>;
|
|
139
|
+
cachedSearch: string;
|
|
140
|
+
cachedReadonlySearchParams: ReadonlyURLSearchParams;
|
|
141
|
+
cachedPathname: string;
|
|
142
|
+
clientParams: Record<string, string | string[]>;
|
|
143
|
+
clientParamsJson: string;
|
|
144
|
+
pendingClientParams: Record<string, string | string[]> | null;
|
|
145
|
+
pendingClientParamsJson: string | null;
|
|
146
|
+
pendingPathname: string | null;
|
|
147
|
+
pendingPathnameNavId: number | null;
|
|
148
|
+
originalPushState: typeof window.history.pushState;
|
|
149
|
+
originalReplaceState: typeof window.history.replaceState;
|
|
150
|
+
patchInstalled: boolean;
|
|
151
|
+
hasPendingNavigationUpdate: boolean;
|
|
152
|
+
suppressUrlNotifyCount: number;
|
|
153
|
+
navigationSnapshotActiveCount: number;
|
|
154
|
+
};
|
|
155
|
+
declare function setMountedSlotsHeader(header: string | null): void;
|
|
156
|
+
declare function getMountedSlotsHeader(): string | null;
|
|
157
|
+
declare function getClientNavigationState(): ClientNavigationState | null;
|
|
130
158
|
/**
|
|
131
159
|
* Mark a navigation snapshot as active. Called before startTransition
|
|
132
160
|
* in renderNavigationPayload. While active, hooks prefer the snapshot
|
|
@@ -146,6 +174,18 @@ declare function setClientParams(params: Record<string, string | string[]>): voi
|
|
|
146
174
|
declare function replaceClientParamsWithoutNotify(params: Record<string, string | string[]>): void;
|
|
147
175
|
/** Get the current client params (for testing referential stability). */
|
|
148
176
|
declare function getClientParams(): Record<string, string | string[]>;
|
|
177
|
+
/**
|
|
178
|
+
* Set the pending pathname for client-side navigation.
|
|
179
|
+
* Strips the base path before storing. Associates the pathname with the given navId
|
|
180
|
+
* so only that navigation (or a newer one) can clear it.
|
|
181
|
+
*/
|
|
182
|
+
declare function setPendingPathname(pathname: string, navId: number): void;
|
|
183
|
+
/**
|
|
184
|
+
* Clear the pending pathname, but only if the given navId matches the one
|
|
185
|
+
* that set it, or if pendingPathnameNavId is null (no active owner).
|
|
186
|
+
* This prevents superseded navigations from clearing state belonging to newer navigations.
|
|
187
|
+
*/
|
|
188
|
+
declare function clearPendingPathname(navId: number): void;
|
|
149
189
|
/**
|
|
150
190
|
* Returns the current pathname.
|
|
151
191
|
* Server: from request context. Client: from window.location.
|
|
@@ -159,7 +199,14 @@ declare function useSearchParams(): ReadonlyURLSearchParams;
|
|
|
159
199
|
* Returns the dynamic params for the current route.
|
|
160
200
|
*/
|
|
161
201
|
declare function useParams<T extends Record<string, string | string[]> = Record<string, string | string[]>>(): T;
|
|
162
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Commit pending client navigation state to committed snapshots.
|
|
204
|
+
*
|
|
205
|
+
* navId is optional: callers that don't own pendingPathname (for example,
|
|
206
|
+
* superseded pre-paint cleanup) may pass undefined to flush snapshot/params
|
|
207
|
+
* state without clearing pendingPathname owned by the active navigation.
|
|
208
|
+
*/
|
|
209
|
+
declare function commitClientNavigationState(navId?: number): void;
|
|
163
210
|
declare function pushHistoryStateWithoutNotify(data: unknown, unused: string, url?: string | URL | null): void;
|
|
164
211
|
declare function replaceHistoryStateWithoutNotify(data: unknown, unused: string, url?: string | URL | null): void;
|
|
165
212
|
/**
|
|
@@ -297,5 +344,5 @@ declare function forbidden(): never;
|
|
|
297
344
|
*/
|
|
298
345
|
declare function unauthorized(): never;
|
|
299
346
|
//#endregion
|
|
300
|
-
export { CachedRscResponse, ClientNavigationRenderSnapshot, GLOBAL_ACCESSORS_KEY, HTTP_ERROR_FALLBACK_ERROR_CODE, MAX_PREFETCH_CACHE_SIZE, NavigationContext, PREFETCH_CACHE_TTL, PrefetchCacheEntry, ReadonlyURLSearchParams, RedirectType, SegmentMap, ServerInsertedHTMLContext, __basePath, _registerStateAccessors, activateNavigationSnapshot, clearServerInsertedHTML, commitClientNavigationState, consumePrefetchResponse, createClientNavigationRenderSnapshot, flushServerInsertedHTML, forbidden, getAccessFallbackHTTPStatus, getClientNavigationRenderContext, getClientParams, getLayoutSegmentContext, getNavigationContext, getPrefetchCache, getPrefetchedUrls, isHTTPAccessFallbackError, navigateClientSide, notFound, permanentRedirect, prefetchRscResponse, pushHistoryStateWithoutNotify, redirect, replaceClientParamsWithoutNotify, replaceHistoryStateWithoutNotify, restoreRscResponse, setClientParams, setNavigationContext, snapshotRscResponse, storePrefetchResponse, toRscUrl, unauthorized, useParams, usePathname, useRouter, useSearchParams, useSelectedLayoutSegment, useSelectedLayoutSegments, useServerInsertedHTML };
|
|
347
|
+
export { CachedRscResponse, ClientNavigationRenderSnapshot, GLOBAL_ACCESSORS_KEY, HTTP_ERROR_FALLBACK_ERROR_CODE, MAX_PREFETCH_CACHE_SIZE, NavigationContext, PREFETCH_CACHE_TTL, PrefetchCacheEntry, ReadonlyURLSearchParams, RedirectType, SegmentMap, ServerInsertedHTMLContext, __basePath, _registerStateAccessors, activateNavigationSnapshot, clearPendingPathname, clearServerInsertedHTML, commitClientNavigationState, consumePrefetchResponse, createClientNavigationRenderSnapshot, flushServerInsertedHTML, forbidden, getAccessFallbackHTTPStatus, getClientNavigationRenderContext, getClientNavigationState, getClientParams, getCurrentInterceptionContext, getCurrentNextUrl, getLayoutSegmentContext, getMountedSlotsHeader, getNavigationContext, getPrefetchCache, getPrefetchedUrls, isHTTPAccessFallbackError, navigateClientSide, notFound, permanentRedirect, prefetchRscResponse, pushHistoryStateWithoutNotify, redirect, replaceClientParamsWithoutNotify, replaceHistoryStateWithoutNotify, restoreRscResponse, setClientParams, setMountedSlotsHeader, setNavigationContext, setPendingPathname, snapshotRscResponse, storePrefetchResponse, toRscUrl, unauthorized, useParams, usePathname, useRouter, useSearchParams, useSelectedLayoutSegment, useSelectedLayoutSegments, useServerInsertedHTML };
|
|
301
348
|
//# sourceMappingURL=navigation.d.ts.map
|
package/dist/shims/navigation.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { stripBasePath } from "../utils/base-path.js";
|
|
2
2
|
import { toBrowserNavigationHref, toSameOriginAppPath } from "./url-utils.js";
|
|
3
3
|
import { notifyAppRouterTransitionStart } from "../client/instrumentation-client-state.js";
|
|
4
|
+
import { createAppPayloadCacheKey } from "../server/app-elements.js";
|
|
4
5
|
import { ReadonlyURLSearchParams } from "./readonly-url-search-params.js";
|
|
5
6
|
import * as React$1 from "react";
|
|
6
7
|
//#region src/shims/navigation.ts
|
|
@@ -115,6 +116,14 @@ function toRscUrl(href) {
|
|
|
115
116
|
const query = qIdx === -1 ? "" : beforeHash.slice(qIdx);
|
|
116
117
|
return (pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname) + ".rsc" + query;
|
|
117
118
|
}
|
|
119
|
+
function getCurrentInterceptionContext() {
|
|
120
|
+
if (isServer) return null;
|
|
121
|
+
return stripBasePath(window.location.pathname, __basePath);
|
|
122
|
+
}
|
|
123
|
+
function getCurrentNextUrl() {
|
|
124
|
+
if (isServer) return "/";
|
|
125
|
+
return window.location.pathname + window.location.search;
|
|
126
|
+
}
|
|
118
127
|
/** Get or create the shared in-memory RSC prefetch cache on window. */
|
|
119
128
|
function getPrefetchCache() {
|
|
120
129
|
if (isServer) return /* @__PURE__ */ new Map();
|
|
@@ -123,7 +132,7 @@ function getPrefetchCache() {
|
|
|
123
132
|
}
|
|
124
133
|
/**
|
|
125
134
|
* Get or create the shared set of already-prefetched RSC URLs on window.
|
|
126
|
-
* Keyed by
|
|
135
|
+
* Keyed by interception-aware cache key so distinct source routes do not alias.
|
|
127
136
|
*/
|
|
128
137
|
function getPrefetchedUrls() {
|
|
129
138
|
if (isServer) return /* @__PURE__ */ new Set();
|
|
@@ -158,23 +167,27 @@ function evictPrefetchCacheIfNeeded() {
|
|
|
158
167
|
* (the caller falls back to a fresh fetch, which is acceptable).
|
|
159
168
|
*
|
|
160
169
|
* Prefer prefetchRscResponse() for new call-sites — it handles the full
|
|
161
|
-
* prefetch lifecycle including dedup
|
|
162
|
-
* backward compatibility and test
|
|
170
|
+
* prefetch lifecycle including dedup and explicit slot context.
|
|
171
|
+
* storePrefetchResponse() is kept for backward compatibility and test
|
|
172
|
+
* helpers. It is slot-unaware: the snapshot's mountedSlotsHeader comes
|
|
173
|
+
* from the response headers, not the caller, so consumePrefetchResponse
|
|
174
|
+
* may reject the entry if the caller's slot context differs.
|
|
163
175
|
*
|
|
164
176
|
* NB: Caller is responsible for managing getPrefetchedUrls() — this
|
|
165
177
|
* function only stores the response in the prefetch cache.
|
|
166
178
|
*/
|
|
167
|
-
function storePrefetchResponse(rscUrl, response) {
|
|
179
|
+
function storePrefetchResponse(rscUrl, response, interceptionContext = null) {
|
|
180
|
+
const cacheKey = createAppPayloadCacheKey(rscUrl, interceptionContext);
|
|
168
181
|
evictPrefetchCacheIfNeeded();
|
|
169
182
|
const entry = { timestamp: Date.now() };
|
|
170
183
|
entry.pending = snapshotRscResponse(response).then((snapshot) => {
|
|
171
184
|
entry.snapshot = snapshot;
|
|
172
185
|
}).catch(() => {
|
|
173
|
-
getPrefetchCache().delete(
|
|
186
|
+
getPrefetchCache().delete(cacheKey);
|
|
174
187
|
}).finally(() => {
|
|
175
188
|
entry.pending = void 0;
|
|
176
189
|
});
|
|
177
|
-
getPrefetchCache().set(
|
|
190
|
+
getPrefetchCache().set(cacheKey, entry);
|
|
178
191
|
}
|
|
179
192
|
/**
|
|
180
193
|
* Snapshot an RSC response to an ArrayBuffer for caching and replay.
|
|
@@ -184,6 +197,7 @@ async function snapshotRscResponse(response) {
|
|
|
184
197
|
return {
|
|
185
198
|
buffer: await response.arrayBuffer(),
|
|
186
199
|
contentType: response.headers.get("content-type") ?? "text/x-component",
|
|
200
|
+
mountedSlotsHeader: response.headers.get("X-Vinext-Mounted-Slots"),
|
|
187
201
|
paramsHeader: response.headers.get("X-Vinext-Params"),
|
|
188
202
|
url: response.url
|
|
189
203
|
};
|
|
@@ -205,6 +219,7 @@ async function snapshotRscResponse(response) {
|
|
|
205
219
|
*/
|
|
206
220
|
function restoreRscResponse(cached, copy = true) {
|
|
207
221
|
const headers = new Headers({ "content-type": cached.contentType });
|
|
222
|
+
if (cached.mountedSlotsHeader != null) headers.set("X-Vinext-Mounted-Slots", cached.mountedSlotsHeader);
|
|
208
223
|
if (cached.paramsHeader != null) headers.set("X-Vinext-Params", cached.paramsHeader);
|
|
209
224
|
return new Response(copy ? cached.buffer.slice(0) : cached.buffer, {
|
|
210
225
|
status: 200,
|
|
@@ -218,23 +233,27 @@ function restoreRscResponse(cached, copy = true) {
|
|
|
218
233
|
* Enforces a maximum cache size to prevent unbounded memory growth on
|
|
219
234
|
* link-heavy pages.
|
|
220
235
|
*/
|
|
221
|
-
function prefetchRscResponse(rscUrl, fetchPromise) {
|
|
236
|
+
function prefetchRscResponse(rscUrl, fetchPromise, interceptionContext = null, mountedSlotsHeader = null) {
|
|
237
|
+
const cacheKey = createAppPayloadCacheKey(rscUrl, interceptionContext);
|
|
222
238
|
const cache = getPrefetchCache();
|
|
223
239
|
const prefetched = getPrefetchedUrls();
|
|
224
240
|
const entry = { timestamp: Date.now() };
|
|
225
241
|
entry.pending = fetchPromise.then(async (response) => {
|
|
226
|
-
if (response.ok) entry.snapshot =
|
|
242
|
+
if (response.ok) entry.snapshot = {
|
|
243
|
+
...await snapshotRscResponse(response),
|
|
244
|
+
mountedSlotsHeader
|
|
245
|
+
};
|
|
227
246
|
else {
|
|
228
|
-
prefetched.delete(
|
|
229
|
-
cache.delete(
|
|
247
|
+
prefetched.delete(cacheKey);
|
|
248
|
+
cache.delete(cacheKey);
|
|
230
249
|
}
|
|
231
250
|
}).catch(() => {
|
|
232
|
-
prefetched.delete(
|
|
233
|
-
cache.delete(
|
|
251
|
+
prefetched.delete(cacheKey);
|
|
252
|
+
cache.delete(cacheKey);
|
|
234
253
|
}).finally(() => {
|
|
235
254
|
entry.pending = void 0;
|
|
236
255
|
});
|
|
237
|
-
cache.set(
|
|
256
|
+
cache.set(cacheKey, entry);
|
|
238
257
|
evictPrefetchCacheIfNeeded();
|
|
239
258
|
}
|
|
240
259
|
/**
|
|
@@ -242,20 +261,32 @@ function prefetchRscResponse(rscUrl, fetchPromise) {
|
|
|
242
261
|
* Only returns settled (non-pending) snapshots synchronously.
|
|
243
262
|
* Returns null if the entry is still in flight or doesn't exist.
|
|
244
263
|
*/
|
|
245
|
-
function consumePrefetchResponse(rscUrl) {
|
|
264
|
+
function consumePrefetchResponse(rscUrl, interceptionContext = null, mountedSlotsHeader = null) {
|
|
265
|
+
const cacheKey = createAppPayloadCacheKey(rscUrl, interceptionContext);
|
|
246
266
|
const cache = getPrefetchCache();
|
|
247
|
-
const entry = cache.get(
|
|
267
|
+
const entry = cache.get(cacheKey);
|
|
248
268
|
if (!entry) return null;
|
|
249
269
|
if (entry.pending) return null;
|
|
250
|
-
cache.delete(
|
|
251
|
-
getPrefetchedUrls().delete(
|
|
270
|
+
cache.delete(cacheKey);
|
|
271
|
+
getPrefetchedUrls().delete(cacheKey);
|
|
252
272
|
if (entry.snapshot) {
|
|
273
|
+
if ((entry.snapshot.mountedSlotsHeader ?? null) !== mountedSlotsHeader) return null;
|
|
253
274
|
if (Date.now() - entry.timestamp >= 3e4) return null;
|
|
254
275
|
return entry.snapshot;
|
|
255
276
|
}
|
|
256
277
|
return null;
|
|
257
278
|
}
|
|
258
279
|
const _CLIENT_NAV_STATE_KEY = Symbol.for("vinext.clientNavigationState");
|
|
280
|
+
const _MOUNTED_SLOTS_HEADER_KEY = Symbol.for("vinext.mountedSlotsHeader");
|
|
281
|
+
function setMountedSlotsHeader(header) {
|
|
282
|
+
if (isServer) return;
|
|
283
|
+
const globalState = window;
|
|
284
|
+
globalState[_MOUNTED_SLOTS_HEADER_KEY] = header;
|
|
285
|
+
}
|
|
286
|
+
function getMountedSlotsHeader() {
|
|
287
|
+
if (isServer) return null;
|
|
288
|
+
return window[_MOUNTED_SLOTS_HEADER_KEY] ?? null;
|
|
289
|
+
}
|
|
259
290
|
function getClientNavigationState() {
|
|
260
291
|
if (isServer) return null;
|
|
261
292
|
const globalState = window;
|
|
@@ -268,6 +299,8 @@ function getClientNavigationState() {
|
|
|
268
299
|
clientParamsJson: "{}",
|
|
269
300
|
pendingClientParams: null,
|
|
270
301
|
pendingClientParamsJson: null,
|
|
302
|
+
pendingPathname: null,
|
|
303
|
+
pendingPathnameNavId: null,
|
|
271
304
|
originalPushState: window.history.pushState.bind(window.history),
|
|
272
305
|
originalReplaceState: window.history.replaceState.bind(window.history),
|
|
273
306
|
patchInstalled: false,
|
|
@@ -413,6 +446,30 @@ function replaceClientParamsWithoutNotify(params) {
|
|
|
413
446
|
function getClientParams() {
|
|
414
447
|
return getClientNavigationState()?.clientParams ?? _fallbackClientParams;
|
|
415
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* Set the pending pathname for client-side navigation.
|
|
451
|
+
* Strips the base path before storing. Associates the pathname with the given navId
|
|
452
|
+
* so only that navigation (or a newer one) can clear it.
|
|
453
|
+
*/
|
|
454
|
+
function setPendingPathname(pathname, navId) {
|
|
455
|
+
const state = getClientNavigationState();
|
|
456
|
+
if (!state) return;
|
|
457
|
+
state.pendingPathname = stripBasePath(pathname, __basePath);
|
|
458
|
+
state.pendingPathnameNavId = navId;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Clear the pending pathname, but only if the given navId matches the one
|
|
462
|
+
* that set it, or if pendingPathnameNavId is null (no active owner).
|
|
463
|
+
* This prevents superseded navigations from clearing state belonging to newer navigations.
|
|
464
|
+
*/
|
|
465
|
+
function clearPendingPathname(navId) {
|
|
466
|
+
const state = getClientNavigationState();
|
|
467
|
+
if (!state) return;
|
|
468
|
+
if (state.pendingPathnameNavId === null || state.pendingPathnameNavId === navId) {
|
|
469
|
+
state.pendingPathname = null;
|
|
470
|
+
state.pendingPathnameNavId = null;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
416
473
|
function getClientParamsSnapshot() {
|
|
417
474
|
return getClientNavigationState()?.clientParams ?? _EMPTY_PARAMS;
|
|
418
475
|
}
|
|
@@ -500,7 +557,14 @@ function withSuppressedUrlNotifications(fn) {
|
|
|
500
557
|
state.suppressUrlNotifyCount -= 1;
|
|
501
558
|
}
|
|
502
559
|
}
|
|
503
|
-
|
|
560
|
+
/**
|
|
561
|
+
* Commit pending client navigation state to committed snapshots.
|
|
562
|
+
*
|
|
563
|
+
* navId is optional: callers that don't own pendingPathname (for example,
|
|
564
|
+
* superseded pre-paint cleanup) may pass undefined to flush snapshot/params
|
|
565
|
+
* state without clearing pendingPathname owned by the active navigation.
|
|
566
|
+
*/
|
|
567
|
+
function commitClientNavigationState(navId) {
|
|
504
568
|
if (isServer) return;
|
|
505
569
|
const state = getClientNavigationState();
|
|
506
570
|
if (!state) return;
|
|
@@ -512,6 +576,10 @@ function commitClientNavigationState() {
|
|
|
512
576
|
state.pendingClientParams = null;
|
|
513
577
|
state.pendingClientParamsJson = null;
|
|
514
578
|
}
|
|
579
|
+
if (state.pendingPathnameNavId === null || navId !== void 0 && state.pendingPathnameNavId === navId) {
|
|
580
|
+
state.pendingPathname = null;
|
|
581
|
+
state.pendingPathnameNavId = null;
|
|
582
|
+
}
|
|
515
583
|
const shouldNotify = urlChanged || state.hasPendingNavigationUpdate;
|
|
516
584
|
state.hasPendingNavigationUpdate = false;
|
|
517
585
|
if (shouldNotify) notifyNavigationListeners();
|
|
@@ -631,14 +699,20 @@ const _appRouter = {
|
|
|
631
699
|
prefetch(href) {
|
|
632
700
|
if (isServer) return;
|
|
633
701
|
const rscUrl = toRscUrl(toBrowserNavigationHref(href, window.location.href, __basePath));
|
|
702
|
+
const interceptionContext = getCurrentInterceptionContext();
|
|
703
|
+
const cacheKey = createAppPayloadCacheKey(rscUrl, interceptionContext);
|
|
634
704
|
const prefetched = getPrefetchedUrls();
|
|
635
|
-
if (prefetched.has(
|
|
636
|
-
prefetched.add(
|
|
705
|
+
if (prefetched.has(cacheKey)) return;
|
|
706
|
+
prefetched.add(cacheKey);
|
|
707
|
+
const mountedSlotsHeader = getMountedSlotsHeader();
|
|
708
|
+
const headers = new Headers({ Accept: "text/x-component" });
|
|
709
|
+
if (mountedSlotsHeader) headers.set("X-Vinext-Mounted-Slots", mountedSlotsHeader);
|
|
710
|
+
if (interceptionContext !== null) headers.set("X-Vinext-Interception-Context", interceptionContext);
|
|
637
711
|
prefetchRscResponse(rscUrl, fetch(rscUrl, {
|
|
638
|
-
headers
|
|
712
|
+
headers,
|
|
639
713
|
credentials: "include",
|
|
640
714
|
priority: "low"
|
|
641
|
-
}));
|
|
715
|
+
}), interceptionContext, mountedSlotsHeader);
|
|
642
716
|
}
|
|
643
717
|
};
|
|
644
718
|
/**
|
|
@@ -838,6 +912,6 @@ if (!isServer) {
|
|
|
838
912
|
}
|
|
839
913
|
}
|
|
840
914
|
//#endregion
|
|
841
|
-
export { GLOBAL_ACCESSORS_KEY, HTTP_ERROR_FALLBACK_ERROR_CODE, MAX_PREFETCH_CACHE_SIZE, PREFETCH_CACHE_TTL, ReadonlyURLSearchParams, RedirectType, ServerInsertedHTMLContext, __basePath, _registerStateAccessors, activateNavigationSnapshot, clearServerInsertedHTML, commitClientNavigationState, consumePrefetchResponse, createClientNavigationRenderSnapshot, flushServerInsertedHTML, forbidden, getAccessFallbackHTTPStatus, getClientNavigationRenderContext, getClientParams, getLayoutSegmentContext, getNavigationContext, getPrefetchCache, getPrefetchedUrls, isHTTPAccessFallbackError, navigateClientSide, notFound, permanentRedirect, prefetchRscResponse, pushHistoryStateWithoutNotify, redirect, replaceClientParamsWithoutNotify, replaceHistoryStateWithoutNotify, restoreRscResponse, setClientParams, setNavigationContext, snapshotRscResponse, storePrefetchResponse, toRscUrl, unauthorized, useParams, usePathname, useRouter, useSearchParams, useSelectedLayoutSegment, useSelectedLayoutSegments, useServerInsertedHTML };
|
|
915
|
+
export { GLOBAL_ACCESSORS_KEY, HTTP_ERROR_FALLBACK_ERROR_CODE, MAX_PREFETCH_CACHE_SIZE, PREFETCH_CACHE_TTL, ReadonlyURLSearchParams, RedirectType, ServerInsertedHTMLContext, __basePath, _registerStateAccessors, activateNavigationSnapshot, clearPendingPathname, clearServerInsertedHTML, commitClientNavigationState, consumePrefetchResponse, createClientNavigationRenderSnapshot, flushServerInsertedHTML, forbidden, getAccessFallbackHTTPStatus, getClientNavigationRenderContext, getClientNavigationState, getClientParams, getCurrentInterceptionContext, getCurrentNextUrl, getLayoutSegmentContext, getMountedSlotsHeader, getNavigationContext, getPrefetchCache, getPrefetchedUrls, isHTTPAccessFallbackError, navigateClientSide, notFound, permanentRedirect, prefetchRscResponse, pushHistoryStateWithoutNotify, redirect, replaceClientParamsWithoutNotify, replaceHistoryStateWithoutNotify, restoreRscResponse, setClientParams, setMountedSlotsHeader, setNavigationContext, setPendingPathname, snapshotRscResponse, storePrefetchResponse, toRscUrl, unauthorized, useParams, usePathname, useRouter, useSearchParams, useSelectedLayoutSegment, useSelectedLayoutSegments, useServerInsertedHTML };
|
|
842
916
|
|
|
843
917
|
//# sourceMappingURL=navigation.js.map
|