Ensure urlPathname is always a pathname (#63846)
### What? This modifies the static generation store to instead store a `url` object with the `pathname` and `search` properties. This corrects the previous behaviour which used the variable `urlPathname` which had ambiguous meanings as it technically contained the search string as well, not just the pathname. In cases during the app render, this still grabs the contents of `url.pathname + url.search` (where `url.search` always has a leading `?` if it has any query parameters, [see the docs](https://developer.mozilla.org/en-US/docs/Web/API/URL/search)) so that it emulates the current behaviour. This allows more specific access though, where now additional parsing can be eliminated which had to strip the query string off of the `urlPathname` in a few places, and more worrisome, still accidentally contained the search string causing errors. ### How? This requires an upstream fix (#64088) which corrected a bug with the store access which had caused some previous test failures (accessing `store.url.pathname` was throwing as `store.url` was undefined on the wrong return, check the upstream PR for more details on that). This also changes out usage of `pagePath` with `route`, and lets it be the fallback (for debugging and error messaging). During static generation, we will provide a value for the page being rendered that's correlated to the particular file on the filesystem that the route is based on: ``` // rendering app/users/[userID]/page.tsx page: /users/[userID] pathname: /users/1, /users/2, etc ``` The `route` is used only for debugging, such as when `generateStaticParams` incorrectly calls `headers()`. This also moves the pathname from the `staticGenerationStore` into the `requestStore`, as it's tied to a given request. Closes NEXT-2965
This commit is contained in:
parent
0fee50ed6e
commit
f0e4298f67
35 changed files with 253 additions and 222 deletions
|
@ -81,7 +81,6 @@ pub async fn get_app_page_entry(
|
|||
indexmap! {
|
||||
"VAR_DEFINITION_PAGE" => page.to_string().into(),
|
||||
"VAR_DEFINITION_PATHNAME" => pathname.clone(),
|
||||
"VAR_ORIGINAL_PATHNAME" => original_name.clone(),
|
||||
// TODO(alexkirsz) Support custom global error.
|
||||
"VAR_MODULE_GLOBAL_ERROR" => "next/dist/client/components/error-boundary".into(),
|
||||
},
|
||||
|
|
|
@ -84,8 +84,7 @@ pub async fn get_app_route_entry(
|
|||
"VAR_DEFINITION_PATHNAME" => pathname.clone(),
|
||||
"VAR_DEFINITION_FILENAME" => path.file_stem().await?.as_ref().unwrap().as_str().into(),
|
||||
// TODO(alexkirsz) Is this necessary?
|
||||
"VAR_DEFINITION_BUNDLE_PATH" => "".into(),
|
||||
"VAR_ORIGINAL_PATHNAME" => original_name.clone(),
|
||||
"VAR_DEFINITION_BUNDLE_PATH" => "".to_string().into(),
|
||||
"VAR_RESOLVED_PAGE_PATH" => path.to_string().await?.clone_value(),
|
||||
"VAR_USERLAND" => INNER.into(),
|
||||
},
|
||||
|
|
|
@ -27,7 +27,6 @@ declare const __next_app_load_chunk__: any
|
|||
// INJECT:__next_app_require__
|
||||
// INJECT:__next_app_load_chunk__
|
||||
|
||||
export const originalPathname = 'VAR_ORIGINAL_PATHNAME'
|
||||
export const __next_app__ = {
|
||||
require: __next_app_require__,
|
||||
loadChunk: __next_app_load_chunk__,
|
||||
|
|
|
@ -35,10 +35,8 @@ const routeModule = new AppRouteRouteModule({
|
|||
const { requestAsyncStorage, staticGenerationAsyncStorage, serverHooks } =
|
||||
routeModule
|
||||
|
||||
const originalPathname = 'VAR_ORIGINAL_PATHNAME'
|
||||
|
||||
function patchFetch() {
|
||||
return _patchFetch({ staticGenerationAsyncStorage })
|
||||
return _patchFetch({ staticGenerationAsyncStorage, requestAsyncStorage })
|
||||
}
|
||||
|
||||
export {
|
||||
|
@ -46,6 +44,5 @@ export {
|
|||
requestAsyncStorage,
|
||||
staticGenerationAsyncStorage,
|
||||
serverHooks,
|
||||
originalPathname,
|
||||
patchFetch,
|
||||
}
|
||||
|
|
|
@ -1384,9 +1384,8 @@ export async function buildAppStaticPaths({
|
|||
return StaticGenerationAsyncStorageWrapper.wrap(
|
||||
ComponentMod.staticGenerationAsyncStorage,
|
||||
{
|
||||
urlPathname: page,
|
||||
page,
|
||||
renderOpts: {
|
||||
originalPathname: page,
|
||||
incrementalCache,
|
||||
supportsDynamicResponse: true,
|
||||
isRevalidate: false,
|
||||
|
|
|
@ -144,7 +144,6 @@ async function createAppRouteCode({
|
|||
VAR_DEFINITION_FILENAME: fileBaseName,
|
||||
VAR_DEFINITION_BUNDLE_PATH: bundlePath,
|
||||
VAR_RESOLVED_PAGE_PATH: resolvedPagePath,
|
||||
VAR_ORIGINAL_PATHNAME: page,
|
||||
},
|
||||
{
|
||||
nextConfigOutput: JSON.stringify(nextConfigOutput),
|
||||
|
@ -772,7 +771,6 @@ const nextAppLoader: AppLoader = async function nextAppLoader() {
|
|||
VAR_DEFINITION_PAGE: page,
|
||||
VAR_DEFINITION_PATHNAME: pathname,
|
||||
VAR_MODULE_GLOBAL_ERROR: treeCodeResult.globalError,
|
||||
VAR_ORIGINAL_PATHNAME: page,
|
||||
},
|
||||
{
|
||||
tree: treeCodeResult.treeCode,
|
||||
|
|
|
@ -10,6 +10,23 @@ import type { DeepReadonly } from '../../shared/lib/deep-readonly'
|
|||
import type { AfterContext } from '../../server/after/after-context'
|
||||
|
||||
export interface RequestStore {
|
||||
/**
|
||||
* The URL of the request. This only specifies the pathname and the search
|
||||
* part of the URL.
|
||||
*/
|
||||
readonly url: {
|
||||
/**
|
||||
* The pathname of the requested URL.
|
||||
*/
|
||||
readonly pathname: string
|
||||
|
||||
/**
|
||||
* The search part of the requested URL. If the request did not provide a
|
||||
* search part, this will be an empty string.
|
||||
*/
|
||||
readonly search: string
|
||||
}
|
||||
|
||||
readonly headers: ReadonlyHeaders
|
||||
readonly cookies: ReadonlyRequestCookies
|
||||
readonly mutableCookies: ResponseCookies
|
||||
|
|
|
@ -10,8 +10,18 @@ import { staticGenerationAsyncStorage } from './static-generation-async-storage-
|
|||
|
||||
export interface StaticGenerationStore {
|
||||
readonly isStaticGeneration: boolean
|
||||
readonly pagePath?: string
|
||||
readonly urlPathname: string
|
||||
|
||||
/**
|
||||
* The page that is being rendered. This relates to the path to the page file.
|
||||
*/
|
||||
readonly page: string
|
||||
|
||||
/**
|
||||
* The route that is being rendered. This is the page property without the
|
||||
* trailing `/page` or `/route` suffix.
|
||||
*/
|
||||
readonly route: string
|
||||
|
||||
readonly incrementalCache?: IncrementalCache
|
||||
readonly isOnDemandRevalidate?: boolean
|
||||
readonly isPrerendering?: boolean
|
||||
|
|
|
@ -69,8 +69,7 @@ export async function exportAppRoute(
|
|||
notFoundRoutes: [],
|
||||
},
|
||||
renderOpts: {
|
||||
experimental: experimental,
|
||||
originalPathname: page,
|
||||
experimental,
|
||||
nextExport: true,
|
||||
supportsDynamicResponse: false,
|
||||
incrementalCache,
|
||||
|
|
|
@ -269,7 +269,6 @@ async function exportPageImpl(
|
|||
fontManifest: optimizeFonts ? requireFontManifest(distDir) : undefined,
|
||||
locale,
|
||||
supportsDynamicResponse: false,
|
||||
originalPathname: page,
|
||||
experimental: {
|
||||
...input.renderOpts.experimental,
|
||||
isRoutePPREnabled,
|
||||
|
|
|
@ -35,11 +35,11 @@ import { isNotFoundError } from '../../client/components/not-found'
|
|||
import type { MetadataContext } from './types/resolvers'
|
||||
|
||||
export function createMetadataContext(
|
||||
urlPathname: string,
|
||||
pathname: string,
|
||||
renderOpts: AppRenderContext['renderOpts']
|
||||
): MetadataContext {
|
||||
return {
|
||||
pathname: urlPathname.split('?')[0],
|
||||
pathname,
|
||||
trailingSlash: renderOpts.trailingSlash,
|
||||
isStandaloneMode: renderOpts.nextConfigOutput === 'standalone',
|
||||
}
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
import { NEXT_RSC_UNION_QUERY } from '../client/components/app-router-headers'
|
||||
|
||||
export const DUMMY_ORIGIN = 'http://n'
|
||||
|
||||
function getUrlWithoutHost(url: string) {
|
||||
return new URL(url, DUMMY_ORIGIN)
|
||||
}
|
||||
|
||||
export function getPathname(url: string) {
|
||||
return getUrlWithoutHost(url).pathname
|
||||
}
|
||||
const DUMMY_ORIGIN = 'http://n'
|
||||
|
||||
export function isFullStringUrl(url: string) {
|
||||
return /https?:\/\//.test(url)
|
||||
|
|
|
@ -463,13 +463,14 @@ describe('createAfterContext', () => {
|
|||
|
||||
const createMockRequestStore = (afterContext: AfterContext): RequestStore => {
|
||||
const partialStore: Partial<RequestStore> = {
|
||||
url: { pathname: '/', search: '' },
|
||||
afterContext: afterContext,
|
||||
assetPrefix: '',
|
||||
reactLoadableManifest: {},
|
||||
draftMode: undefined,
|
||||
}
|
||||
|
||||
return new Proxy(partialStore, {
|
||||
return new Proxy(partialStore as RequestStore, {
|
||||
get(target, key) {
|
||||
if (key in target) {
|
||||
return target[key as keyof typeof target]
|
||||
|
@ -478,5 +479,5 @@ const createMockRequestStore = (afterContext: AfterContext): RequestStore => {
|
|||
`RequestStore property not mocked: '${typeof key === 'symbol' ? key.toString() : key}'`
|
||||
)
|
||||
},
|
||||
}) as RequestStore
|
||||
})
|
||||
}
|
||||
|
|
|
@ -143,6 +143,7 @@ function wrapRequestStoreForAfterCallbacks(
|
|||
requestStore: RequestStore
|
||||
): RequestStore {
|
||||
return {
|
||||
url: requestStore.url,
|
||||
get headers() {
|
||||
return requestStore.headers
|
||||
},
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { getExpectedRequestStore } from '../../client/components/request-async-storage.external'
|
||||
import { staticGenerationAsyncStorage } from '../../client/components/static-generation-async-storage.external'
|
||||
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout'
|
||||
import { getPathname } from '../../lib/url'
|
||||
|
||||
import { markCurrentScopeAsDynamic } from '../app-render/dynamic-rendering'
|
||||
|
||||
|
@ -27,9 +26,8 @@ export function unstable_after<T>(task: AfterTask<T>) {
|
|||
|
||||
if (staticGenerationStore) {
|
||||
if (staticGenerationStore.forceStatic) {
|
||||
const pathname = getPathname(staticGenerationStore.urlPathname)
|
||||
throw new StaticGenBailoutError(
|
||||
`Route ${pathname} with \`dynamic = "force-static"\` couldn't be rendered statically because it used \`${callingExpression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
|
||||
`Route ${staticGenerationStore.route} with \`dynamic = "force-static"\` couldn't be rendered statically because it used \`${callingExpression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
|
||||
)
|
||||
} else {
|
||||
markCurrentScopeAsDynamic(staticGenerationStore, callingExpression)
|
||||
|
|
|
@ -70,7 +70,6 @@ import {
|
|||
import { getSegmentParam } from './get-segment-param'
|
||||
import { getScriptNonceFromHeader } from './get-script-nonce-from-header'
|
||||
import { parseAndValidateFlightRouterState } from './parse-and-validate-flight-router-state'
|
||||
import { validateURL } from './validate-url'
|
||||
import { createFlightRouterStateFromLoaderTree } from './create-flight-router-state-from-loader-tree'
|
||||
import { handleAction } from './action-handler'
|
||||
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
|
||||
|
@ -116,6 +115,7 @@ import {
|
|||
import { createServerModuleMap } from './action-utils'
|
||||
import { isNodeNextRequest } from '../base-http/helpers'
|
||||
import { parseParameter } from '../../shared/lib/router/utils/route-regex'
|
||||
import { parseRelativeUrl } from '../../shared/lib/router/utils/parse-relative-url'
|
||||
|
||||
export type GetDynamicParamFromSegment = (
|
||||
// [slug] / [[slug]] / [...slug]
|
||||
|
@ -319,7 +319,7 @@ async function generateFlight(
|
|||
},
|
||||
getDynamicParamFromSegment,
|
||||
appUsingSizeAdjustment,
|
||||
staticGenerationStore: { urlPathname },
|
||||
requestStore: { url },
|
||||
query,
|
||||
requestId,
|
||||
flightRouterState,
|
||||
|
@ -329,7 +329,7 @@ async function generateFlight(
|
|||
const [MetadataTree, MetadataOutlet] = createMetadataComponents({
|
||||
tree: loaderTree,
|
||||
query,
|
||||
metadataContext: createMetadataContext(urlPathname, ctx.renderOpts),
|
||||
metadataContext: createMetadataContext(url.pathname, ctx.renderOpts),
|
||||
getDynamicParamFromSegment,
|
||||
appUsingSizeAdjustment,
|
||||
createDynamicallyTrackedSearchParams,
|
||||
|
@ -441,7 +441,7 @@ async function ReactServerApp({ tree, ctx, asNotFound }: ReactServerAppProps) {
|
|||
GlobalError,
|
||||
createDynamicallyTrackedSearchParams,
|
||||
},
|
||||
staticGenerationStore: { urlPathname },
|
||||
requestStore: { url },
|
||||
} = ctx
|
||||
const initialTree = createFlightRouterStateFromLoaderTree(
|
||||
tree,
|
||||
|
@ -453,7 +453,7 @@ async function ReactServerApp({ tree, ctx, asNotFound }: ReactServerAppProps) {
|
|||
tree,
|
||||
errorType: asNotFound ? 'not-found' : undefined,
|
||||
query,
|
||||
metadataContext: createMetadataContext(urlPathname, ctx.renderOpts),
|
||||
metadataContext: createMetadataContext(url.pathname, ctx.renderOpts),
|
||||
getDynamicParamFromSegment: getDynamicParamFromSegment,
|
||||
appUsingSizeAdjustment: appUsingSizeAdjustment,
|
||||
createDynamicallyTrackedSearchParams,
|
||||
|
@ -485,7 +485,7 @@ async function ReactServerApp({ tree, ctx, asNotFound }: ReactServerAppProps) {
|
|||
<AppRouter
|
||||
buildId={ctx.renderOpts.buildId}
|
||||
assetPrefix={ctx.assetPrefix}
|
||||
initialCanonicalUrl={urlPathname}
|
||||
initialCanonicalUrl={url.pathname + url.search}
|
||||
// This is the router state tree.
|
||||
initialTree={initialTree}
|
||||
// This is the tree of React nodes that are seeded into the cache
|
||||
|
@ -530,14 +530,14 @@ async function ReactServerError({
|
|||
GlobalError,
|
||||
createDynamicallyTrackedSearchParams,
|
||||
},
|
||||
staticGenerationStore: { urlPathname },
|
||||
requestStore: { url },
|
||||
requestId,
|
||||
res,
|
||||
} = ctx
|
||||
|
||||
const [MetadataTree] = createMetadataComponents({
|
||||
tree,
|
||||
metadataContext: createMetadataContext(urlPathname, ctx.renderOpts),
|
||||
metadataContext: createMetadataContext(url.pathname, ctx.renderOpts),
|
||||
errorType,
|
||||
query,
|
||||
getDynamicParamFromSegment,
|
||||
|
@ -579,7 +579,7 @@ async function ReactServerError({
|
|||
<AppRouter
|
||||
buildId={ctx.renderOpts.buildId}
|
||||
assetPrefix={ctx.assetPrefix}
|
||||
initialCanonicalUrl={urlPathname}
|
||||
initialCanonicalUrl={url.pathname + url.search}
|
||||
initialTree={initialTree}
|
||||
initialHead={head}
|
||||
initialLayerAssets={null}
|
||||
|
@ -1407,7 +1407,7 @@ async function renderToHTMLOrFlightImpl(
|
|||
])
|
||||
}
|
||||
|
||||
addImplicitTags(staticGenerationStore)
|
||||
addImplicitTags(staticGenerationStore, requestStore)
|
||||
|
||||
if (staticGenerationStore.tags) {
|
||||
metadata.fetchTags = staticGenerationStore.tags.join(',')
|
||||
|
@ -1498,17 +1498,20 @@ export const renderToHTMLOrFlight: AppPageRender = (
|
|||
query,
|
||||
renderOpts
|
||||
) => {
|
||||
// TODO: this includes query string, should it?
|
||||
const pathname = validateURL(req.url)
|
||||
if (!req.url) {
|
||||
throw new Error('Invalid URL')
|
||||
}
|
||||
|
||||
const url = parseRelativeUrl(req.url, undefined, false)
|
||||
|
||||
return RequestAsyncStorageWrapper.wrap(
|
||||
renderOpts.ComponentMod.requestAsyncStorage,
|
||||
{ req, res, renderOpts },
|
||||
{ req, url, res, renderOpts },
|
||||
(requestStore) =>
|
||||
StaticGenerationAsyncStorageWrapper.wrap(
|
||||
renderOpts.ComponentMod.staticGenerationAsyncStorage,
|
||||
{
|
||||
urlPathname: pathname,
|
||||
page: renderOpts.routeModule.definition.page,
|
||||
renderOpts,
|
||||
requestEndedState: { ended: false },
|
||||
},
|
||||
|
|
|
@ -227,10 +227,7 @@ async function createComponentTreeInternal({
|
|||
}
|
||||
|
||||
if (typeof layoutOrPageMod?.revalidate !== 'undefined') {
|
||||
validateRevalidate(
|
||||
layoutOrPageMod?.revalidate,
|
||||
staticGenerationStore.urlPathname
|
||||
)
|
||||
validateRevalidate(layoutOrPageMod?.revalidate, staticGenerationStore.route)
|
||||
}
|
||||
|
||||
if (typeof layoutOrPageMod?.revalidate === 'number') {
|
||||
|
@ -537,7 +534,7 @@ async function createComponentTreeInternal({
|
|||
<Postpone
|
||||
prerenderState={staticGenerationStore.prerenderState}
|
||||
reason='dynamic = "force-dynamic" was used'
|
||||
pathname={staticGenerationStore.urlPathname}
|
||||
route={staticGenerationStore.route}
|
||||
/>,
|
||||
loadingData,
|
||||
],
|
||||
|
|
|
@ -26,7 +26,6 @@ import React from 'react'
|
|||
import type { StaticGenerationStore } from '../../client/components/static-generation-async-storage.external'
|
||||
import { DynamicServerError } from '../../client/components/hooks-server-context'
|
||||
import { StaticGenBailoutError } from '../../client/components/static-generation-bailout'
|
||||
import { getPathname } from '../../lib/url'
|
||||
|
||||
const hasPostpone = typeof React.unstable_postpone === 'function'
|
||||
|
||||
|
@ -86,11 +85,9 @@ export function markCurrentScopeAsDynamic(
|
|||
// or it's static and it should not throw or postpone here.
|
||||
if (store.forceDynamic || store.forceStatic) return
|
||||
|
||||
const pathname = getPathname(store.urlPathname)
|
||||
|
||||
if (store.dynamicShouldError) {
|
||||
throw new StaticGenBailoutError(
|
||||
`Route ${pathname} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
|
||||
`Route ${store.route} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -101,7 +98,7 @@ export function markCurrentScopeAsDynamic(
|
|||
// We track that we had a dynamic scope that postponed.
|
||||
// This will be used by the renderer to decide whether
|
||||
// the prerender requires a resume
|
||||
postponeWithTracking(store.prerenderState, expression, pathname)
|
||||
postponeWithTracking(store.prerenderState, expression, store.route)
|
||||
}
|
||||
|
||||
store.revalidate = 0
|
||||
|
@ -109,7 +106,7 @@ export function markCurrentScopeAsDynamic(
|
|||
if (store.isStaticGeneration) {
|
||||
// We aren't prerendering but we are generating a static page. We need to bail out of static generation
|
||||
const err = new DynamicServerError(
|
||||
`Route ${pathname} couldn't be rendered statically because it used ${expression}. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`
|
||||
`Route ${store.route} couldn't be rendered statically because it used ${expression}. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`
|
||||
)
|
||||
store.dynamicUsageDescription = expression
|
||||
store.dynamicUsageStack = err.stack
|
||||
|
@ -131,14 +128,13 @@ export function trackDynamicDataAccessed(
|
|||
store: StaticGenerationStore,
|
||||
expression: string
|
||||
): void {
|
||||
const pathname = getPathname(store.urlPathname)
|
||||
if (store.isUnstableCacheCallback) {
|
||||
throw new Error(
|
||||
`Route ${pathname} used "${expression}" inside a function cached with "unstable_cache(...)". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "${expression}" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache`
|
||||
`Route ${store.route} used "${expression}" inside a function cached with "unstable_cache(...)". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use "${expression}" outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache`
|
||||
)
|
||||
} else if (store.dynamicShouldError) {
|
||||
throw new StaticGenBailoutError(
|
||||
`Route ${pathname} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
|
||||
`Route ${store.route} with \`dynamic = "error"\` couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
|
||||
)
|
||||
} else if (
|
||||
// We are in a prerender (PPR enabled, during build)
|
||||
|
@ -147,14 +143,14 @@ export function trackDynamicDataAccessed(
|
|||
// We track that we had a dynamic scope that postponed.
|
||||
// This will be used by the renderer to decide whether
|
||||
// the prerender requires a resume
|
||||
postponeWithTracking(store.prerenderState, expression, pathname)
|
||||
postponeWithTracking(store.prerenderState, expression, store.route)
|
||||
} else {
|
||||
store.revalidate = 0
|
||||
|
||||
if (store.isStaticGeneration) {
|
||||
// We aren't prerendering but we are generating a static page. We need to bail out of static generation
|
||||
const err = new DynamicServerError(
|
||||
`Route ${pathname} couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`
|
||||
`Route ${store.route} couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error`
|
||||
)
|
||||
store.dynamicUsageDescription = expression
|
||||
store.dynamicUsageStack = err.stack
|
||||
|
@ -170,24 +166,24 @@ export function trackDynamicDataAccessed(
|
|||
type PostponeProps = {
|
||||
reason: string
|
||||
prerenderState: PrerenderState
|
||||
pathname: string
|
||||
route: string
|
||||
}
|
||||
export function Postpone({
|
||||
reason,
|
||||
prerenderState,
|
||||
pathname,
|
||||
route,
|
||||
}: PostponeProps): never {
|
||||
postponeWithTracking(prerenderState, reason, pathname)
|
||||
postponeWithTracking(prerenderState, reason, route)
|
||||
}
|
||||
|
||||
function postponeWithTracking(
|
||||
prerenderState: PrerenderState,
|
||||
expression: string,
|
||||
pathname: string
|
||||
route: string
|
||||
): never {
|
||||
assertPostpone()
|
||||
const reason =
|
||||
`Route ${pathname} needs to bail out of prerendering at this point because it used ${expression}. ` +
|
||||
`Route ${route} needs to bail out of prerendering at this point because it used ${expression}. ` +
|
||||
`React throws this special object to indicate where. It should not be caught by ` +
|
||||
`your own try/catch. Learn more: https://nextjs.org/docs/messages/ppr-caught-error`
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ function patchCacheScopeSupportIntoReact() {
|
|||
// patchFetch makes use of APIs such as `React.unstable_postpone` which are only available
|
||||
// in the experimental channel of React, so export it from here so that it comes from the bundled runtime
|
||||
function patchFetch() {
|
||||
return _patchFetch({ staticGenerationAsyncStorage })
|
||||
return _patchFetch({ staticGenerationAsyncStorage, requestAsyncStorage })
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
@ -143,7 +143,6 @@ export interface RenderOptsPartial {
|
|||
nextExport?: boolean
|
||||
nextConfigOutput?: 'standalone' | 'export'
|
||||
appDirDevErrorLogger?: (err: any) => Promise<void>
|
||||
originalPathname?: string
|
||||
isDraftMode?: boolean
|
||||
deploymentId?: string
|
||||
onUpdateCookies?: (cookies: string[]) => void
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
const DUMMY_ORIGIN = 'http://n'
|
||||
const INVALID_URL_MESSAGE = 'Invalid request URL'
|
||||
|
||||
export function validateURL(url: string | undefined): string {
|
||||
if (!url) {
|
||||
throw new Error(INVALID_URL_MESSAGE)
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(url, DUMMY_ORIGIN)
|
||||
// Avoid origin change by extra slashes in pathname
|
||||
if (parsed.origin !== DUMMY_ORIGIN) {
|
||||
throw new Error(INVALID_URL_MESSAGE)
|
||||
}
|
||||
return url
|
||||
} catch {
|
||||
throw new Error(INVALID_URL_MESSAGE)
|
||||
}
|
||||
}
|
|
@ -40,19 +40,39 @@ function getMutableCookies(
|
|||
return MutableRequestCookiesAdapter.wrap(cookies, onUpdateCookies)
|
||||
}
|
||||
|
||||
export type WrapperRenderOpts = Omit<RenderOpts, 'experimental'> &
|
||||
RequestLifecycleOpts &
|
||||
export type WrapperRenderOpts = RequestLifecycleOpts &
|
||||
Partial<
|
||||
Pick<
|
||||
RenderOpts,
|
||||
'ComponentMod' // can be undefined in a route handler
|
||||
| 'ComponentMod'
|
||||
| 'onUpdateCookies'
|
||||
| 'assetPrefix'
|
||||
| 'reactLoadableManifest'
|
||||
>
|
||||
> & {
|
||||
experimental: Pick<RenderOpts['experimental'], 'after'>
|
||||
previewProps?: __ApiPreviewProps
|
||||
}
|
||||
|
||||
export type RequestContext = {
|
||||
req: IncomingMessage | BaseNextRequest | NextRequest
|
||||
/**
|
||||
* The URL of the request. This only specifies the pathname and the search
|
||||
* part of the URL. This is only undefined when generating static paths (ie,
|
||||
* there is no request in progress, nor do we know one).
|
||||
*/
|
||||
url: {
|
||||
/**
|
||||
* The pathname of the requested URL.
|
||||
*/
|
||||
pathname: string
|
||||
|
||||
/**
|
||||
* The search part of the requested URL. If the request did not provide a
|
||||
* search part, this will be an empty string.
|
||||
*/
|
||||
search?: string
|
||||
}
|
||||
res?: ServerResponse | BaseNextResponse
|
||||
renderOpts?: WrapperRenderOpts
|
||||
}
|
||||
|
@ -72,16 +92,9 @@ export const RequestAsyncStorageWrapper: AsyncStorageWrapper<
|
|||
*/
|
||||
wrap<Result>(
|
||||
storage: AsyncLocalStorage<RequestStore>,
|
||||
{ req, res, renderOpts }: RequestContext,
|
||||
{ req, url, res, renderOpts }: RequestContext,
|
||||
callback: (store: RequestStore) => Result
|
||||
): Result {
|
||||
let previewProps: __ApiPreviewProps | undefined = undefined
|
||||
|
||||
if (renderOpts && 'previewProps' in renderOpts) {
|
||||
// TODO: investigate why previewProps isn't on RenderOpts
|
||||
previewProps = (renderOpts as any).previewProps
|
||||
}
|
||||
|
||||
const [wrapWithAfter, afterContext] = createAfterWrapper(renderOpts)
|
||||
|
||||
function defaultOnUpdateCookies(cookies: string[]) {
|
||||
|
@ -98,6 +111,10 @@ export const RequestAsyncStorageWrapper: AsyncStorageWrapper<
|
|||
} = {}
|
||||
|
||||
const store: RequestStore = {
|
||||
// Rather than just using the whole `url` here, we pull the parts we want
|
||||
// to ensure we don't use parts of the URL that we shouldn't. This also
|
||||
// lets us avoid requiring an empty string for `search` in the type.
|
||||
url: { pathname: url.pathname, search: url.search ?? '' },
|
||||
get headers() {
|
||||
if (!cache.headers) {
|
||||
// Seal the headers object that'll freeze out any methods that could
|
||||
|
@ -154,7 +171,7 @@ export const RequestAsyncStorageWrapper: AsyncStorageWrapper<
|
|||
get draftMode() {
|
||||
if (!cache.draftMode) {
|
||||
cache.draftMode = new DraftModeProvider(
|
||||
previewProps,
|
||||
renderOpts?.previewProps,
|
||||
req,
|
||||
this.cookies,
|
||||
this.mutableCookies
|
||||
|
|
|
@ -4,12 +4,16 @@ import type { AsyncLocalStorage } from 'async_hooks'
|
|||
import type { IncrementalCache } from '../lib/incremental-cache'
|
||||
import type { RenderOptsPartial } from '../app-render/types'
|
||||
|
||||
import { createPrerenderState } from '../../server/app-render/dynamic-rendering'
|
||||
import { createPrerenderState } from '../app-render/dynamic-rendering'
|
||||
import type { FetchMetric } from '../base-http'
|
||||
import type { RequestLifecycleOpts } from '../base-server'
|
||||
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
|
||||
|
||||
export type StaticGenerationContext = {
|
||||
urlPathname: string
|
||||
/**
|
||||
* The page that is being rendered. This relates to the path to the page file.
|
||||
*/
|
||||
page: string
|
||||
requestEndedState?: { ended?: boolean }
|
||||
renderOpts: {
|
||||
incrementalCache?: IncrementalCache
|
||||
|
@ -40,7 +44,6 @@ export type StaticGenerationContext = {
|
|||
// Pull some properties from RenderOptsPartial so that the docs are also
|
||||
// mirrored.
|
||||
RenderOptsPartial,
|
||||
| 'originalPathname'
|
||||
| 'supportsDynamicResponse'
|
||||
| 'isRevalidate'
|
||||
| 'nextExport'
|
||||
|
@ -56,7 +59,7 @@ export const StaticGenerationAsyncStorageWrapper: AsyncStorageWrapper<
|
|||
> = {
|
||||
wrap<Result>(
|
||||
storage: AsyncLocalStorage<StaticGenerationStore>,
|
||||
{ urlPathname, renderOpts, requestEndedState }: StaticGenerationContext,
|
||||
{ page, renderOpts, requestEndedState }: StaticGenerationContext,
|
||||
callback: (store: StaticGenerationStore) => Result
|
||||
): Result {
|
||||
/**
|
||||
|
@ -88,8 +91,8 @@ export const StaticGenerationAsyncStorageWrapper: AsyncStorageWrapper<
|
|||
|
||||
const store: StaticGenerationStore = {
|
||||
isStaticGeneration,
|
||||
urlPathname,
|
||||
pagePath: renderOpts.originalPathname,
|
||||
page,
|
||||
route: normalizeAppPath(page),
|
||||
incrementalCache:
|
||||
// we fallback to a global incremental cache for edge-runtime locally
|
||||
// so that it can access the fs cache without mocks
|
||||
|
|
|
@ -2340,7 +2340,6 @@ export default abstract class Server<
|
|||
// it is not a dynamic RSC request then it is a revalidation
|
||||
// request.
|
||||
isRevalidate: isSSG && !postponed && !isDynamicRSCRequest,
|
||||
originalPathname: components.ComponentMod.originalPathname,
|
||||
serverActions: this.nextConfig.experimental.serverActions,
|
||||
}
|
||||
: {}),
|
||||
|
@ -2409,7 +2408,6 @@ export default abstract class Server<
|
|||
experimental: {
|
||||
after: renderOpts.experimental.after,
|
||||
},
|
||||
originalPathname: components.ComponentMod.originalPathname,
|
||||
supportsDynamicResponse,
|
||||
incrementalCache,
|
||||
isRevalidate: isSSG,
|
||||
|
|
|
@ -15,6 +15,10 @@ import * as Log from '../../build/output/log'
|
|||
import { markCurrentScopeAsDynamic } from '../app-render/dynamic-rendering'
|
||||
import type { FetchMetric } from '../base-http'
|
||||
import { createDedupeFetch } from './dedupe-fetch'
|
||||
import type {
|
||||
RequestAsyncStorage,
|
||||
RequestStore,
|
||||
} from '../../client/components/request-async-storage.external'
|
||||
|
||||
const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'
|
||||
|
||||
|
@ -34,7 +38,7 @@ function isPatchedFetch(
|
|||
|
||||
export function validateRevalidate(
|
||||
revalidateVal: unknown,
|
||||
pathname: string
|
||||
route: string
|
||||
): undefined | number | false {
|
||||
try {
|
||||
let normalizedRevalidate: false | number | undefined = undefined
|
||||
|
@ -49,7 +53,7 @@ export function validateRevalidate(
|
|||
normalizedRevalidate = revalidateVal
|
||||
} else if (typeof revalidateVal !== 'undefined') {
|
||||
throw new Error(
|
||||
`Invalid revalidate value "${revalidateVal}" on "${pathname}", must be a non-negative number or "false"`
|
||||
`Invalid revalidate value "${revalidateVal}" on "${route}", must be a non-negative number or "false"`
|
||||
)
|
||||
}
|
||||
return normalizedRevalidate
|
||||
|
@ -127,35 +131,35 @@ const getDerivedTags = (pathname: string): string[] => {
|
|||
return derivedTags
|
||||
}
|
||||
|
||||
export function addImplicitTags(staticGenerationStore: StaticGenerationStore) {
|
||||
export function addImplicitTags(
|
||||
staticGenerationStore: StaticGenerationStore,
|
||||
requestStore: RequestStore | undefined
|
||||
) {
|
||||
const newTags: string[] = []
|
||||
const { pagePath, urlPathname } = staticGenerationStore
|
||||
const { page } = staticGenerationStore
|
||||
|
||||
if (!Array.isArray(staticGenerationStore.tags)) {
|
||||
staticGenerationStore.tags = []
|
||||
}
|
||||
// Ini the tags array if it doesn't exist.
|
||||
staticGenerationStore.tags ??= []
|
||||
|
||||
if (pagePath) {
|
||||
const derivedTags = getDerivedTags(pagePath)
|
||||
|
||||
for (let tag of derivedTags) {
|
||||
tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${tag}`
|
||||
if (!staticGenerationStore.tags?.includes(tag)) {
|
||||
staticGenerationStore.tags.push(tag)
|
||||
}
|
||||
newTags.push(tag)
|
||||
}
|
||||
}
|
||||
|
||||
if (urlPathname) {
|
||||
const parsedPathname = new URL(urlPathname, 'http://n').pathname
|
||||
|
||||
const tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${parsedPathname}`
|
||||
// Add the derived tags from the page.
|
||||
const derivedTags = getDerivedTags(page)
|
||||
for (let tag of derivedTags) {
|
||||
tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${tag}`
|
||||
if (!staticGenerationStore.tags?.includes(tag)) {
|
||||
staticGenerationStore.tags.push(tag)
|
||||
}
|
||||
newTags.push(tag)
|
||||
}
|
||||
|
||||
// Add the tags from the pathname.
|
||||
if (requestStore?.url.pathname) {
|
||||
const tag = `${NEXT_CACHE_IMPLICIT_TAG_ID}${requestStore.url.pathname}`
|
||||
if (!staticGenerationStore.tags?.includes(tag)) {
|
||||
staticGenerationStore.tags.push(tag)
|
||||
}
|
||||
newTags.push(tag)
|
||||
}
|
||||
|
||||
return newTags
|
||||
}
|
||||
|
||||
|
@ -210,11 +214,12 @@ function trackFetchMetric(
|
|||
|
||||
interface PatchableModule {
|
||||
staticGenerationAsyncStorage: StaticGenerationAsyncStorage
|
||||
requestAsyncStorage: RequestAsyncStorage
|
||||
}
|
||||
|
||||
function createPatchedFetcher(
|
||||
originFetch: Fetcher,
|
||||
{ staticGenerationAsyncStorage }: PatchableModule
|
||||
{ staticGenerationAsyncStorage, requestAsyncStorage }: PatchableModule
|
||||
): PatchedFetcher {
|
||||
// Create the patched fetch function. We don't set the type here, as it's
|
||||
// verified as the return value of this function.
|
||||
|
@ -260,6 +265,7 @@ function createPatchedFetcher(
|
|||
}
|
||||
|
||||
const staticGenerationStore = staticGenerationAsyncStorage.getStore()
|
||||
const requestStore = requestAsyncStorage.getStore()
|
||||
|
||||
// If the staticGenerationStore is not available, we can't do any
|
||||
// special treatment of fetch, therefore fallback to the original
|
||||
|
@ -311,7 +317,10 @@ function createPatchedFetcher(
|
|||
}
|
||||
}
|
||||
}
|
||||
const implicitTags = addImplicitTags(staticGenerationStore)
|
||||
const implicitTags = addImplicitTags(
|
||||
staticGenerationStore,
|
||||
requestStore
|
||||
)
|
||||
|
||||
const pageFetchCacheMode = staticGenerationStore.fetchCache
|
||||
const isUsingNoStore = !!staticGenerationStore.isUnstableNoStore
|
||||
|
@ -327,7 +336,7 @@ function createPatchedFetcher(
|
|||
// we only want to warn if the user is explicitly setting a cache value
|
||||
if (!(isRequestInput && currentFetchCacheConfig === 'default')) {
|
||||
Log.warn(
|
||||
`fetch for ${fetchUrl} on ${staticGenerationStore.urlPathname} specified "cache: ${currentFetchCacheConfig}" and "revalidate: ${currentFetchRevalidate}", only one should be specified.`
|
||||
`fetch for ${fetchUrl} on ${staticGenerationStore.route} specified "cache: ${currentFetchCacheConfig}" and "revalidate: ${currentFetchRevalidate}", only one should be specified.`
|
||||
)
|
||||
}
|
||||
currentFetchCacheConfig = undefined
|
||||
|
@ -359,7 +368,7 @@ function createPatchedFetcher(
|
|||
|
||||
finalRevalidate = validateRevalidate(
|
||||
currentFetchRevalidate,
|
||||
staticGenerationStore.urlPathname
|
||||
staticGenerationStore.route
|
||||
)
|
||||
|
||||
const _headers = getRequestMeta('headers')
|
||||
|
@ -484,11 +493,7 @@ function createPatchedFetcher(
|
|||
if (finalRevalidate === 0) {
|
||||
markCurrentScopeAsDynamic(
|
||||
staticGenerationStore,
|
||||
`revalidate: 0 fetch ${input}${
|
||||
staticGenerationStore.urlPathname
|
||||
? ` ${staticGenerationStore.urlPathname}`
|
||||
: ''
|
||||
}`
|
||||
`revalidate: 0 fetch ${input} ${staticGenerationStore.route}`
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -711,11 +716,7 @@ function createPatchedFetcher(
|
|||
// If enabled, we should bail out of static generation.
|
||||
markCurrentScopeAsDynamic(
|
||||
staticGenerationStore,
|
||||
`no-store fetch ${input}${
|
||||
staticGenerationStore.urlPathname
|
||||
? ` ${staticGenerationStore.urlPathname}`
|
||||
: ''
|
||||
}`
|
||||
`no-store fetch ${input} ${staticGenerationStore.route}`
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -731,11 +732,7 @@ function createPatchedFetcher(
|
|||
// If enabled, we should bail out of static generation.
|
||||
markCurrentScopeAsDynamic(
|
||||
staticGenerationStore,
|
||||
`revalidate: 0 fetch ${input}${
|
||||
staticGenerationStore.urlPathname
|
||||
? ` ${staticGenerationStore.urlPathname}`
|
||||
: ''
|
||||
}`
|
||||
`revalidate: 0 fetch ${input} ${staticGenerationStore.route}`
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ export type LoadComponentsReturnType<NextModule = any> = {
|
|||
getStaticPaths?: GetStaticPaths
|
||||
getServerSideProps?: GetServerSideProps
|
||||
ComponentMod: NextModule
|
||||
routeModule?: RouteModule
|
||||
routeModule: RouteModule
|
||||
isAppPath?: boolean
|
||||
page: string
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ export type LoadComponentsReturnType = {
|
|||
getStaticPaths?: GetStaticPaths
|
||||
getServerSideProps?: GetServerSideProps
|
||||
ComponentMod: any
|
||||
routeModule?: RouteModule
|
||||
routeModule: RouteModule
|
||||
isAppPath?: boolean
|
||||
page: string
|
||||
}
|
||||
|
|
|
@ -257,19 +257,18 @@ export class AppRouteRouteModule extends RouteModule<
|
|||
// Get the context for the request.
|
||||
const requestContext: RequestContext = {
|
||||
req: rawRequest,
|
||||
}
|
||||
|
||||
requestContext.renderOpts = {
|
||||
// @ts-expect-error TODO: types for renderOpts should include previewProps
|
||||
previewProps: context.prerenderManifest.preview,
|
||||
waitUntil: context.renderOpts.waitUntil,
|
||||
onClose: context.renderOpts.onClose,
|
||||
experimental: context.renderOpts.experimental,
|
||||
url: rawRequest.nextUrl,
|
||||
renderOpts: {
|
||||
previewProps: context.prerenderManifest.preview,
|
||||
waitUntil: context.renderOpts.waitUntil,
|
||||
onClose: context.renderOpts.onClose,
|
||||
experimental: context.renderOpts.experimental,
|
||||
},
|
||||
}
|
||||
|
||||
// Get the context for the static generation.
|
||||
const staticGenerationContext: StaticGenerationContext = {
|
||||
urlPathname: rawRequest.nextUrl.pathname,
|
||||
page: this.definition.page,
|
||||
renderOpts: context.renderOpts,
|
||||
}
|
||||
|
||||
|
@ -288,7 +287,7 @@ export class AppRouteRouteModule extends RouteModule<
|
|||
RequestAsyncStorageWrapper.wrap(
|
||||
this.requestAsyncStorage,
|
||||
requestContext,
|
||||
() =>
|
||||
(requestStore) =>
|
||||
StaticGenerationAsyncStorageWrapper.wrap(
|
||||
this.staticGenerationAsyncStorage,
|
||||
staticGenerationContext,
|
||||
|
@ -375,6 +374,7 @@ export class AppRouteRouteModule extends RouteModule<
|
|||
patchFetch({
|
||||
staticGenerationAsyncStorage:
|
||||
this.staticGenerationAsyncStorage,
|
||||
requestAsyncStorage: this.requestAsyncStorage,
|
||||
})
|
||||
const res = await handler(request, {
|
||||
params: context.params
|
||||
|
@ -398,14 +398,13 @@ export class AppRouteRouteModule extends RouteModule<
|
|||
),
|
||||
])
|
||||
|
||||
addImplicitTags(staticGenerationStore)
|
||||
addImplicitTags(staticGenerationStore, requestStore)
|
||||
;(context.renderOpts as any).fetchTags =
|
||||
staticGenerationStore.tags?.join(',')
|
||||
|
||||
// It's possible cookies were set in the handler, so we need
|
||||
// to merge the modified cookies and the returned response
|
||||
// here.
|
||||
const requestStore = this.requestAsyncStorage.getStore()
|
||||
if (requestStore && requestStore.mutableCookies) {
|
||||
const headers = new Headers(res.headers)
|
||||
if (
|
||||
|
|
|
@ -240,20 +240,22 @@ export async function adapter(
|
|||
},
|
||||
async () => {
|
||||
try {
|
||||
const previewProps = prerenderManifest?.preview || {
|
||||
previewModeId: 'development-id',
|
||||
previewModeEncryptionKey: '',
|
||||
previewModeSigningKey: '',
|
||||
}
|
||||
|
||||
return await RequestAsyncStorageWrapper.wrap(
|
||||
requestAsyncStorage,
|
||||
{
|
||||
req: request,
|
||||
url: request.nextUrl,
|
||||
renderOpts: {
|
||||
onUpdateCookies: (cookies) => {
|
||||
cookiesFromResponse = cookies
|
||||
},
|
||||
// @ts-expect-error TODO: investigate why previewProps isn't on RenderOpts
|
||||
previewProps: prerenderManifest?.preview || {
|
||||
previewModeId: 'development-id',
|
||||
previewModeEncryptionKey: '',
|
||||
previewModeSigningKey: '',
|
||||
},
|
||||
previewProps,
|
||||
waitUntil,
|
||||
onClose: closeController
|
||||
? closeController.onClose.bind(closeController)
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
NEXT_CACHE_IMPLICIT_TAG_ID,
|
||||
NEXT_CACHE_SOFT_TAG_MAX_LENGTH,
|
||||
} from '../../../lib/constants'
|
||||
import { getPathname } from '../../../lib/url'
|
||||
import { staticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external'
|
||||
|
||||
/**
|
||||
|
@ -51,9 +50,7 @@ function revalidate(tag: string, expression: string) {
|
|||
|
||||
if (store.isUnstableCacheCallback) {
|
||||
throw new Error(
|
||||
`Route ${getPathname(
|
||||
store.urlPathname
|
||||
)} used "${expression}" inside a function cached with "unstable_cache(...)" which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
|
||||
`Route ${store.route} used "${expression}" inside a function cached with "unstable_cache(...)" which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering`
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
validateTags,
|
||||
} from '../../lib/patch-fetch'
|
||||
import { staticGenerationAsyncStorage } from '../../../client/components/static-generation-async-storage.external'
|
||||
import { requestAsyncStorage } from '../../../client/components/request-async-storage.external'
|
||||
|
||||
type Callback = (...args: any[]) => Promise<any>
|
||||
|
||||
|
@ -90,13 +91,15 @@ export function unstable_cache<T extends Callback>(
|
|||
}`
|
||||
|
||||
const cachedCb = async (...args: any[]) => {
|
||||
const store = staticGenerationAsyncStorage.getStore()
|
||||
const staticGenerationStore = staticGenerationAsyncStorage.getStore()
|
||||
const requestStore = requestAsyncStorage.getStore()
|
||||
|
||||
// We must be able to find the incremental cache otherwise we throw
|
||||
const maybeIncrementalCache:
|
||||
| import('../../lib/incremental-cache').IncrementalCache
|
||||
| undefined =
|
||||
store?.incrementalCache || (globalThis as any).__incrementalCache
|
||||
staticGenerationStore?.incrementalCache ||
|
||||
(globalThis as any).__incrementalCache
|
||||
|
||||
if (!maybeIncrementalCache) {
|
||||
throw new Error(
|
||||
|
@ -105,10 +108,14 @@ export function unstable_cache<T extends Callback>(
|
|||
}
|
||||
const incrementalCache = maybeIncrementalCache
|
||||
|
||||
const { pathname, searchParams } = new URL(
|
||||
store?.urlPathname || '/',
|
||||
'http://n'
|
||||
)
|
||||
// If there's no request store, we aren't in a request (or we're not in app
|
||||
// router) and if there's no static generation store, we aren't in app
|
||||
// router. Default to an empty pathname and search params when there's no
|
||||
// request store or static generation store available.
|
||||
const pathname =
|
||||
requestStore?.url.pathname ?? staticGenerationStore?.route ?? ''
|
||||
const searchParams = new URLSearchParams(requestStore?.url.search ?? '')
|
||||
|
||||
const sortedSearchKeys = [...searchParams.keys()].sort((a, b) => {
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
|
@ -123,10 +130,13 @@ export function unstable_cache<T extends Callback>(
|
|||
const cacheKey = await incrementalCache.fetchCacheKey(invocationKey)
|
||||
// $urlWithPath,$sortedQueryStringKeys,$hashOfEveryThingElse
|
||||
const fetchUrl = `unstable_cache ${pathname}${sortedSearch.length ? '?' : ''}${sortedSearch} ${cb.name ? ` ${cb.name}` : cacheKey}`
|
||||
const fetchIdx = (store ? store.nextFetchId : noStoreFetchIdx) ?? 1
|
||||
const fetchIdx =
|
||||
(staticGenerationStore
|
||||
? staticGenerationStore.nextFetchId
|
||||
: noStoreFetchIdx) ?? 1
|
||||
|
||||
if (store) {
|
||||
store.nextFetchId = fetchIdx + 1
|
||||
if (staticGenerationStore) {
|
||||
staticGenerationStore.nextFetchId = fetchIdx + 1
|
||||
|
||||
// We are in an App Router context. We try to return the cached entry if it exists and is valid
|
||||
// If the entry is fresh we return it. If the entry is stale we return it but revalidate the entry in
|
||||
|
@ -135,43 +145,43 @@ export function unstable_cache<T extends Callback>(
|
|||
// We update the store's revalidate property if the option.revalidate is a higher precedence
|
||||
if (typeof options.revalidate === 'number') {
|
||||
if (
|
||||
typeof store.revalidate === 'number' &&
|
||||
store.revalidate < options.revalidate
|
||||
typeof staticGenerationStore.revalidate === 'number' &&
|
||||
staticGenerationStore.revalidate < options.revalidate
|
||||
) {
|
||||
// The store is already revalidating on a shorter time interval, leave it alone
|
||||
} else {
|
||||
store.revalidate = options.revalidate
|
||||
staticGenerationStore.revalidate = options.revalidate
|
||||
}
|
||||
} else if (
|
||||
options.revalidate === false &&
|
||||
typeof store.revalidate === 'undefined'
|
||||
typeof staticGenerationStore.revalidate === 'undefined'
|
||||
) {
|
||||
// The store has not defined revalidate type so we can use the false option
|
||||
store.revalidate = options.revalidate
|
||||
staticGenerationStore.revalidate = options.revalidate
|
||||
}
|
||||
|
||||
// We need to accumulate the tags for this invocation within the store
|
||||
if (!store.tags) {
|
||||
store.tags = tags.slice()
|
||||
if (!staticGenerationStore.tags) {
|
||||
staticGenerationStore.tags = tags.slice()
|
||||
} else {
|
||||
for (const tag of tags) {
|
||||
// @TODO refactor tags to be a set to avoid this O(n) lookup
|
||||
if (!store.tags.includes(tag)) {
|
||||
store.tags.push(tag)
|
||||
if (!staticGenerationStore.tags.includes(tag)) {
|
||||
staticGenerationStore.tags.push(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
// @TODO check on this API. addImplicitTags mutates the store and returns the implicit tags. The naming
|
||||
// of this function is potentially a little confusing
|
||||
const implicitTags = addImplicitTags(store)
|
||||
const implicitTags = addImplicitTags(staticGenerationStore, requestStore)
|
||||
|
||||
if (
|
||||
// when we are nested inside of other unstable_cache's
|
||||
// we should bypass cache similar to fetches
|
||||
store.fetchCache !== 'force-no-store' &&
|
||||
!store.isOnDemandRevalidate &&
|
||||
staticGenerationStore.fetchCache !== 'force-no-store' &&
|
||||
!staticGenerationStore.isOnDemandRevalidate &&
|
||||
!incrementalCache.isOnDemandRevalidate &&
|
||||
!store.isDraftMode
|
||||
!staticGenerationStore.isDraftMode
|
||||
) {
|
||||
// We attempt to get the current cache entry from the incremental cache.
|
||||
const cacheEntry = await incrementalCache.get(cacheKey, {
|
||||
|
@ -203,15 +213,15 @@ export function unstable_cache<T extends Callback>(
|
|||
: undefined
|
||||
if (cacheEntry.isStale) {
|
||||
// In App Router we return the stale result and revalidate in the background
|
||||
if (!store.pendingRevalidates) {
|
||||
store.pendingRevalidates = {}
|
||||
if (!staticGenerationStore.pendingRevalidates) {
|
||||
staticGenerationStore.pendingRevalidates = {}
|
||||
}
|
||||
// We run the cache function asynchronously and save the result when it completes
|
||||
store.pendingRevalidates[invocationKey] =
|
||||
staticGenerationStore.pendingRevalidates[invocationKey] =
|
||||
staticGenerationAsyncStorage
|
||||
.run(
|
||||
{
|
||||
...store,
|
||||
...staticGenerationStore,
|
||||
// force any nested fetches to bypass cache so they revalidate
|
||||
// when the unstable_cache call is revalidated
|
||||
fetchCache: 'force-no-store',
|
||||
|
@ -248,7 +258,7 @@ export function unstable_cache<T extends Callback>(
|
|||
// If we got this far then we had an invalid cache entry and need to generate a new one
|
||||
const result = await staticGenerationAsyncStorage.run(
|
||||
{
|
||||
...store,
|
||||
...staticGenerationStore,
|
||||
// force any nested fetches to bypass cache so they revalidate
|
||||
// when the unstable_cache call is revalidated
|
||||
fetchCache: 'force-no-store',
|
||||
|
@ -279,7 +289,9 @@ export function unstable_cache<T extends Callback>(
|
|||
|
||||
// @TODO check on this API. addImplicitTags mutates the store and returns the implicit tags. The naming
|
||||
// of this function is potentially a little confusing
|
||||
const implicitTags = store && addImplicitTags(store)
|
||||
const implicitTags =
|
||||
staticGenerationStore &&
|
||||
addImplicitTags(staticGenerationStore, requestStore)
|
||||
|
||||
const cacheEntry = await incrementalCache.get(cacheKey, {
|
||||
kindHint: 'fetch',
|
||||
|
@ -325,7 +337,8 @@ export function unstable_cache<T extends Callback>(
|
|||
// when the unstable_cache call is revalidated
|
||||
fetchCache: 'force-no-store',
|
||||
isUnstableCacheCallback: true,
|
||||
urlPathname: '/',
|
||||
route: '/',
|
||||
page: '/',
|
||||
isStaticGeneration: false,
|
||||
prerenderState: null,
|
||||
},
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { parseRelativeUrl } from './parse-relative-url'
|
||||
|
||||
describe('relative urls', () => {
|
||||
it('should return valid pathname', () => {
|
||||
expect(parseRelativeUrl('/').pathname).toBe('/')
|
||||
expect(parseRelativeUrl('/abc').pathname).toBe('/abc')
|
||||
})
|
||||
|
||||
it('should throw for invalid pathname', () => {
|
||||
expect(() => parseRelativeUrl('//**y/\\')).toThrow()
|
||||
expect(() => parseRelativeUrl('//google.com')).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('query parsing', () => {
|
||||
it('should parse query string', () => {
|
||||
expect(parseRelativeUrl('/?a=1&b=2').query).toEqual({ a: '1', b: '2' })
|
||||
expect(parseRelativeUrl('/').query).toEqual({})
|
||||
})
|
||||
})
|
|
@ -18,8 +18,19 @@ export interface ParsedRelativeUrl {
|
|||
*/
|
||||
export function parseRelativeUrl(
|
||||
url: string,
|
||||
base?: string
|
||||
): ParsedRelativeUrl {
|
||||
base?: string,
|
||||
parseQuery?: true
|
||||
): ParsedRelativeUrl
|
||||
export function parseRelativeUrl(
|
||||
url: string,
|
||||
base: string | undefined,
|
||||
parseQuery: false
|
||||
): Omit<ParsedRelativeUrl, 'query'>
|
||||
export function parseRelativeUrl(
|
||||
url: string,
|
||||
base?: string,
|
||||
parseQuery = true
|
||||
): ParsedRelativeUrl | Omit<ParsedRelativeUrl, 'query'> {
|
||||
const globalBase = new URL(
|
||||
typeof window === 'undefined' ? 'http://n' : getLocationOrigin()
|
||||
)
|
||||
|
@ -36,14 +47,16 @@ export function parseRelativeUrl(
|
|||
url,
|
||||
resolvedBase
|
||||
)
|
||||
|
||||
if (origin !== globalBase.origin) {
|
||||
throw new Error(`invariant: invalid relative URL, router received ${url}`)
|
||||
}
|
||||
|
||||
return {
|
||||
pathname,
|
||||
query: searchParamsToUrlQuery(searchParams),
|
||||
query: parseQuery ? searchParamsToUrlQuery(searchParams) : undefined,
|
||||
search,
|
||||
hash,
|
||||
href: href.slice(globalBase.origin.length),
|
||||
href: href.slice(origin.length),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { matchHas, prepareDestination } from './prepare-destination'
|
|||
import { removeTrailingSlash } from './remove-trailing-slash'
|
||||
import { normalizeLocalePath } from '../../i18n/normalize-locale-path'
|
||||
import { removeBasePath } from '../../../../client/remove-base-path'
|
||||
import { parseRelativeUrl } from './parse-relative-url'
|
||||
import { parseRelativeUrl, type ParsedRelativeUrl } from './parse-relative-url'
|
||||
|
||||
export default function resolveRewrites(
|
||||
asPath: string,
|
||||
|
@ -20,7 +20,7 @@ export default function resolveRewrites(
|
|||
locales?: string[]
|
||||
): {
|
||||
matchedPage: boolean
|
||||
parsedAs: ReturnType<typeof parseRelativeUrl>
|
||||
parsedAs: ParsedRelativeUrl
|
||||
asPath: string
|
||||
resolvedHref?: string
|
||||
externalDest?: boolean
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import { validateURL } from 'next/dist/server/app-render/validate-url'
|
||||
|
||||
describe('validateUrl', () => {
|
||||
it('should return valid pathname', () => {
|
||||
expect(validateURL('/')).toBe('/')
|
||||
expect(validateURL('/abc')).toBe('/abc')
|
||||
})
|
||||
|
||||
it('should throw for invalid pathname', () => {
|
||||
expect(() => validateURL('//**y/\\')).toThrow()
|
||||
expect(() => validateURL('//google.com')).toThrow()
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue