From 2a9457364ca11c53814819d321814b58fe68288a Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Wed, 7 Feb 2024 12:31:16 -0800 Subject: [PATCH] consolidate prefetch utils --- packages/next/src/build/index.ts | 50 +----------- .../router-reducer/prefetch-cache-utils.ts | 79 +++++++++++++++++++ .../router-reducer/router-reducer-types.ts | 7 ++ packages/next/src/lib/build-custom-route.ts | 60 ++++++++++++++ 4 files changed, 147 insertions(+), 49 deletions(-) create mode 100644 packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts create mode 100644 packages/next/src/lib/build-custom-route.ts diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 72ccd89f0d2514..4cff69e26fb082 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -19,7 +19,6 @@ import { defaultConfig } from '../server/config-shared' import devalue from 'next/dist/compiled/devalue' import findUp from 'next/dist/compiled/find-up' import { nanoid } from 'next/dist/compiled/nanoid/index.cjs' -import { pathToRegexp } from 'next/dist/compiled/path-to-regexp' import path from 'path' import { STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR, @@ -41,9 +40,7 @@ import type { Redirect, Rewrite, RouteHas, - RouteType, } from '../lib/load-custom-routes' -import { getRedirectStatus, modifyRouteRegex } from '../lib/redirect-status' import { nonNullable } from '../lib/non-nullable' import { recursiveDelete } from '../lib/recursive-delete' import { verifyPartytownSetup } from '../lib/verify-partytown-setup' @@ -167,6 +164,7 @@ import { hasCustomExportOutput } from '../export/utils' import { interopDefault } from '../lib/interop-default' import { formatDynamicImportPath } from '../lib/format-dynamic-import-path' import { isInterceptionRouteAppPath } from '../server/future/helpers/interception-routes' +import { buildCustomRoute } from '../lib/build-custom-route' interface ExperimentalBypassForInfo { experimentalBypassFor?: RouteHas[] @@ -272,52 +270,6 @@ export type RoutesManifest = { caseSensitive?: boolean } -export function buildCustomRoute( - type: 'header', - route: Header -): ManifestHeaderRoute -export function buildCustomRoute( - type: 'rewrite', - route: Rewrite -): ManifestRewriteRoute -export function buildCustomRoute( - type: 'redirect', - route: Redirect, - restrictedRedirectPaths: string[] -): ManifestRedirectRoute -export function buildCustomRoute( - type: RouteType, - route: Redirect | Rewrite | Header, - restrictedRedirectPaths?: string[] -): ManifestHeaderRoute | ManifestRewriteRoute | ManifestRedirectRoute { - const compiled = pathToRegexp(route.source, [], { - strict: true, - sensitive: false, - delimiter: '/', // default is `/#?`, but Next does not pass query info - }) - - let source = compiled.source - if (!route.internal) { - source = modifyRouteRegex( - source, - type === 'redirect' ? restrictedRedirectPaths : undefined - ) - } - - const regex = normalizeRouteRegex(source) - - if (type !== 'redirect') { - return { ...route, regex } - } - - return { - ...route, - statusCode: getRedirectStatus(route as Redirect), - permanent: undefined, - regex, - } -} - function pageToRoute(page: string) { const routeRegex = getNamedRouteRegex(page, true) return { diff --git a/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts b/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts new file mode 100644 index 00000000000000..d91aaca4a9d7b2 --- /dev/null +++ b/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts @@ -0,0 +1,79 @@ +import { + PrefetchCacheEntryStatus, + type AppRouterState, + type PrefetchCacheEntry, +} from './router-reducer-types' +import { addPathPrefix } from '../../../shared/lib/router/utils/add-path-prefix' +import { pathHasPrefix } from '../../../shared/lib/router/utils/path-has-prefix' +import { createHrefFromUrl } from './create-href-from-url' + +/** + * Creates a cache key for the router prefetch cache + * + * @param url - The URL being navigated to + * @param nextUrl - an internal URL, primarily used for handling rewrites. Defaults to '/'. + * @return The generated prefetch cache key. + */ +export function createPrefetchCacheKey(url: URL, nextUrl: string | null) { + const pathnameFromUrl = createHrefFromUrl( + url, + // Ensures the hash is not part of the cache key as it does not impact the server fetch + false + ) + + // delimit the prefix so we don't conflict with other pages + const nextUrlPrefix = `${nextUrl}%` + + // Route interception depends on `nextUrl` values which aren't a 1:1 mapping to a URL + // The cache key that we store needs to use `nextUrl` to properly distinguish cache entries + if (nextUrl && !pathHasPrefix(pathnameFromUrl, nextUrl)) { + return addPathPrefix(pathnameFromUrl, nextUrlPrefix) + } + + return pathnameFromUrl +} + +export function prunePrefetchCache( + prefetchCache: AppRouterState['prefetchCache'] +) { + for (const [href, prefetchCacheEntry] of prefetchCache) { + if ( + getPrefetchEntryCacheStatus(prefetchCacheEntry) === + PrefetchCacheEntryStatus.expired + ) { + prefetchCache.delete(href) + } + } +} + +const FIVE_MINUTES = 5 * 60 * 1000 +const THIRTY_SECONDS = 30 * 1000 + +export function getPrefetchEntryCacheStatus({ + kind, + prefetchTime, + lastUsedTime, +}: PrefetchCacheEntry): PrefetchCacheEntryStatus { + // if the cache entry was prefetched or read less than 30s ago, then we want to re-use it + if (Date.now() < (lastUsedTime ?? prefetchTime) + THIRTY_SECONDS) { + return lastUsedTime + ? PrefetchCacheEntryStatus.reusable + : PrefetchCacheEntryStatus.fresh + } + + // if the cache entry was prefetched less than 5 mins ago, then we want to re-use only the loading state + if (kind === 'auto') { + if (Date.now() < prefetchTime + FIVE_MINUTES) { + return PrefetchCacheEntryStatus.stale + } + } + + // if the cache entry was prefetched less than 5 mins ago and was a "full" prefetch, then we want to re-use it "full + if (kind === 'full') { + if (Date.now() < prefetchTime + FIVE_MINUTES) { + return PrefetchCacheEntryStatus.reusable + } + } + + return PrefetchCacheEntryStatus.expired +} diff --git a/packages/next/src/client/components/router-reducer/router-reducer-types.ts b/packages/next/src/client/components/router-reducer/router-reducer-types.ts index ebf7f944c548f4..ab034432ebaf35 100644 --- a/packages/next/src/client/components/router-reducer/router-reducer-types.ts +++ b/packages/next/src/client/components/router-reducer/router-reducer-types.ts @@ -204,6 +204,13 @@ export type PrefetchCacheEntry = { lastUsedTime: number | null } +export enum PrefetchCacheEntryStatus { + fresh = 'fresh', + reusable = 'reusable', + expired = 'expired', + stale = 'stale', +} + /** * Handles keeping the state of app-router. */ diff --git a/packages/next/src/lib/build-custom-route.ts b/packages/next/src/lib/build-custom-route.ts new file mode 100644 index 00000000000000..b5eec9564a76ea --- /dev/null +++ b/packages/next/src/lib/build-custom-route.ts @@ -0,0 +1,60 @@ +import { pathToRegexp } from 'next/dist/compiled/path-to-regexp' +import type { + ManifestHeaderRoute, + ManifestRedirectRoute, + ManifestRewriteRoute, +} from '../build' +import { + normalizeRouteRegex, + type Header, + type Redirect, + type Rewrite, + type RouteType, +} from './load-custom-routes' +import { getRedirectStatus, modifyRouteRegex } from './redirect-status' + +export function buildCustomRoute( + type: 'header', + route: Header +): ManifestHeaderRoute +export function buildCustomRoute( + type: 'rewrite', + route: Rewrite +): ManifestRewriteRoute +export function buildCustomRoute( + type: 'redirect', + route: Redirect, + restrictedRedirectPaths: string[] +): ManifestRedirectRoute +export function buildCustomRoute( + type: RouteType, + route: Redirect | Rewrite | Header, + restrictedRedirectPaths?: string[] +): ManifestHeaderRoute | ManifestRewriteRoute | ManifestRedirectRoute { + const compiled = pathToRegexp(route.source, [], { + strict: true, + sensitive: false, + delimiter: '/', // default is `/#?`, but Next does not pass query info + }) + + let source = compiled.source + if (!route.internal) { + source = modifyRouteRegex( + source, + type === 'redirect' ? restrictedRedirectPaths : undefined + ) + } + + const regex = normalizeRouteRegex(source) + + if (type !== 'redirect') { + return { ...route, regex } + } + + return { + ...route, + statusCode: getRedirectStatus(route as Redirect), + permanent: undefined, + regex, + } +}