import { workAsyncStorage, type WorkStore, } from '../app-render/work-async-storage.external' import type { OpaqueFallbackRouteParams } from './fallback-params' import { ReflectAdapter } from '../web/spec-extension/adapters/reflect' import { throwToInterruptStaticGeneration, postponeWithTracking, delayUntilRuntimeStage, } from '../app-render/dynamic-rendering' import { workUnitAsyncStorage, type PrerenderStorePPR, type PrerenderStoreLegacy, type StaticPrerenderStoreModern, type StaticPrerenderStore, throwInvariantForMissingStore, type PrerenderStoreModernRuntime, type RequestStore, } from '../app-render/work-unit-async-storage.external' import { InvariantError } from '../../shared/lib/invariant-error' import { describeStringPropertyAccess, wellKnownProperties, } from '../../shared/lib/utils/reflect-utils' import { makeDevtoolsIOAwarePromise, makeHangingPromise, } from '../dynamic-rendering-utils' import { createDedupedByCallsiteServerErrorLoggerDev } from '../create-deduped-by-callsite-server-error-logger' import { dynamicAccessAsyncStorage } from '../app-render/dynamic-access-async-storage.external' import { RenderStage } from '../app-render/staged-rendering' export type ParamValue = string | Array | undefined export type Params = Record export function createParamsFromClient( underlyingParams: Params, workStore: WorkStore ): Promise { const workUnitStore = workUnitAsyncStorage.getStore() if (workUnitStore) { switch (workUnitStore.type) { case 'prerender': case 'prerender-client': case 'prerender-ppr': case 'prerender-legacy': return createStaticPrerenderParams( underlyingParams, workStore, workUnitStore ) case 'cache': case 'private-cache': case 'unstable-cache': throw new InvariantError( 'createParamsFromClient should not be called in cache contexts.' ) case 'prerender-runtime': throw new InvariantError( 'createParamsFromClient should not be called in a runtime prerender.' ) case 'request': if (process.env.NODE_ENV === 'development') { // Semantically we only need the dev tracking when running in `next dev` // but since you would never use next dev with production NODE_ENV we use this // as a proxy so we can statically exclude this code from production builds. const devFallbackParams = workUnitStore.devFallbackParams return createRenderParamsInDev( underlyingParams, devFallbackParams, workStore, workUnitStore ) } else { return createRenderParamsInProd(underlyingParams) } default: workUnitStore satisfies never } } throwInvariantForMissingStore() } // generateMetadata always runs in RSC context so it is equivalent to a Server Page Component export type CreateServerParamsForMetadata = typeof createServerParamsForMetadata export const createServerParamsForMetadata = createServerParamsForServerSegment // routes always runs in RSC context so it is equivalent to a Server Page Component export function createServerParamsForRoute( underlyingParams: Params, workStore: WorkStore ): Promise { const workUnitStore = workUnitAsyncStorage.getStore() if (workUnitStore) { switch (workUnitStore.type) { case 'prerender': case 'prerender-client': case 'prerender-ppr': case 'prerender-legacy': return createStaticPrerenderParams( underlyingParams, workStore, workUnitStore ) case 'cache': case 'private-cache': case 'unstable-cache': throw new InvariantError( 'createServerParamsForRoute should not be called in cache contexts.' ) case 'prerender-runtime': return createRuntimePrerenderParams(underlyingParams, workUnitStore) case 'request': if (process.env.NODE_ENV === 'development') { // Semantically we only need the dev tracking when running in `next dev` // but since you would never use next dev with production NODE_ENV we use this // as a proxy so we can statically exclude this code from production builds. const devFallbackParams = workUnitStore.devFallbackParams return createRenderParamsInDev( underlyingParams, devFallbackParams, workStore, workUnitStore ) } else { return createRenderParamsInProd(underlyingParams) } default: workUnitStore satisfies never } } throwInvariantForMissingStore() } export function createServerParamsForServerSegment( underlyingParams: Params, workStore: WorkStore ): Promise { const workUnitStore = workUnitAsyncStorage.getStore() if (workUnitStore) { switch (workUnitStore.type) { case 'prerender': case 'prerender-client': case 'prerender-ppr': case 'prerender-legacy': return createStaticPrerenderParams( underlyingParams, workStore, workUnitStore ) case 'cache': case 'private-cache': case 'unstable-cache': throw new InvariantError( 'createServerParamsForServerSegment should not be called in cache contexts.' ) case 'prerender-runtime': return createRuntimePrerenderParams(underlyingParams, workUnitStore) case 'request': if (process.env.NODE_ENV === 'development') { // Semantically we only need the dev tracking when running in `next dev` // but since you would never use next dev with production NODE_ENV we use this // as a proxy so we can statically exclude this code from production builds. const devFallbackParams = workUnitStore.devFallbackParams return createRenderParamsInDev( underlyingParams, devFallbackParams, workStore, workUnitStore ) } else { return createRenderParamsInProd(underlyingParams) } default: workUnitStore satisfies never } } throwInvariantForMissingStore() } export function createPrerenderParamsForClientSegment( underlyingParams: Params ): Promise { const workStore = workAsyncStorage.getStore() if (!workStore) { throw new InvariantError( 'Missing workStore in createPrerenderParamsForClientSegment' ) } const workUnitStore = workUnitAsyncStorage.getStore() if (workUnitStore) { switch (workUnitStore.type) { case 'prerender': case 'prerender-client': const fallbackParams = workUnitStore.fallbackRouteParams if (fallbackParams) { for (let key in underlyingParams) { if (fallbackParams.has(key)) { // This params object has one or more fallback params, so we need // to consider the awaiting of this params object "dynamic". Since // we are in cacheComponents mode we encode this as a promise that never // resolves. return makeHangingPromise( workUnitStore.renderSignal, workStore.route, '`params`' ) } } } break case 'cache': case 'private-cache': case 'unstable-cache': throw new InvariantError( 'createPrerenderParamsForClientSegment should not be called in cache contexts.' ) case 'prerender-ppr': case 'prerender-legacy': case 'prerender-runtime': case 'request': break default: workUnitStore satisfies never } } // We're prerendering in a mode that does not abort. We resolve the promise without // any tracking because we're just transporting a value from server to client where the tracking // will be applied. return Promise.resolve(underlyingParams) } function createStaticPrerenderParams( underlyingParams: Params, workStore: WorkStore, prerenderStore: StaticPrerenderStore ): Promise { switch (prerenderStore.type) { case 'prerender': case 'prerender-client': { const fallbackParams = prerenderStore.fallbackRouteParams if (fallbackParams) { for (const key in underlyingParams) { if (fallbackParams.has(key)) { // This params object has one or more fallback params, so we need // to consider the awaiting of this params object "dynamic". Since // we are in cacheComponents mode we encode this as a promise that never // resolves. return makeHangingParams( underlyingParams, workStore, prerenderStore ) } } } break } case 'prerender-ppr': { const fallbackParams = prerenderStore.fallbackRouteParams if (fallbackParams) { for (const key in underlyingParams) { if (fallbackParams.has(key)) { return makeErroringParams( underlyingParams, fallbackParams, workStore, prerenderStore ) } } } break } case 'prerender-legacy': break default: prerenderStore satisfies never } return makeUntrackedParams(underlyingParams) } function createRuntimePrerenderParams( underlyingParams: Params, workUnitStore: PrerenderStoreModernRuntime ): Promise { return delayUntilRuntimeStage( workUnitStore, makeUntrackedParams(underlyingParams) ) } function createRenderParamsInProd(underlyingParams: Params): Promise { return makeUntrackedParams(underlyingParams) } function createRenderParamsInDev( underlyingParams: Params, devFallbackParams: OpaqueFallbackRouteParams | null | undefined, workStore: WorkStore, requestStore: RequestStore ): Promise { let hasFallbackParams = false if (devFallbackParams) { for (let key in underlyingParams) { if (devFallbackParams.has(key)) { hasFallbackParams = true break } } } return makeDynamicallyTrackedParamsWithDevWarnings( underlyingParams, hasFallbackParams, workStore, requestStore ) } interface CacheLifetime {} const CachedParams = new WeakMap>() const fallbackParamsProxyHandler: ProxyHandler> = { get: function get(target, prop, receiver) { if (prop === 'then' || prop === 'catch' || prop === 'finally') { const originalMethod = ReflectAdapter.get(target, prop, receiver) return { [prop]: (...args: unknown[]) => { const store = dynamicAccessAsyncStorage.getStore() if (store) { store.abortController.abort( new Error(`Accessed fallback \`params\` during prerendering.`) ) } return new Proxy( originalMethod.apply(target, args), fallbackParamsProxyHandler ) }, }[prop] } return ReflectAdapter.get(target, prop, receiver) }, } function makeHangingParams( underlyingParams: Params, workStore: WorkStore, prerenderStore: StaticPrerenderStoreModern ): Promise { const cachedParams = CachedParams.get(underlyingParams) if (cachedParams) { return cachedParams } const promise = new Proxy( makeHangingPromise( prerenderStore.renderSignal, workStore.route, '`params`' ), fallbackParamsProxyHandler ) CachedParams.set(underlyingParams, promise) return promise } function makeErroringParams( underlyingParams: Params, fallbackParams: OpaqueFallbackRouteParams, workStore: WorkStore, prerenderStore: PrerenderStorePPR | PrerenderStoreLegacy ): Promise { const cachedParams = CachedParams.get(underlyingParams) if (cachedParams) { return cachedParams } const augmentedUnderlying = { ...underlyingParams } // We don't use makeResolvedReactPromise here because params // supports copying with spread and we don't want to unnecessarily // instrument the promise with spreadable properties of ReactPromise. const promise = Promise.resolve(augmentedUnderlying) CachedParams.set(underlyingParams, promise) Object.keys(underlyingParams).forEach((prop) => { if (wellKnownProperties.has(prop)) { // These properties cannot be shadowed because they need to be the // true underlying value for Promises to work correctly at runtime } else { if (fallbackParams.has(prop)) { Object.defineProperty(augmentedUnderlying, prop, { get() { const expression = describeStringPropertyAccess('params', prop) // In most dynamic APIs we also throw if `dynamic = "error"` however // for params is only dynamic when we're generating a fallback shell // and even when `dynamic = "error"` we still support generating dynamic // fallback shells // TODO remove this comment when cacheComponents is the default since there // will be no `dynamic = "error"` if (prerenderStore.type === 'prerender-ppr') { // PPR Prerender (no cacheComponents) postponeWithTracking( workStore.route, expression, prerenderStore.dynamicTracking ) } else { // Legacy Prerender throwToInterruptStaticGeneration( expression, workStore, prerenderStore ) } }, enumerable: true, }) } } }) return promise } function makeUntrackedParams(underlyingParams: Params): Promise { const cachedParams = CachedParams.get(underlyingParams) if (cachedParams) { return cachedParams } const promise = Promise.resolve(underlyingParams) CachedParams.set(underlyingParams, promise) return promise } function makeDynamicallyTrackedParamsWithDevWarnings( underlyingParams: Params, hasFallbackParams: boolean, workStore: WorkStore, requestStore: RequestStore ): Promise { if (requestStore.asyncApiPromises && hasFallbackParams) { // We wrap each instance of params in a `new Promise()`, because deduping // them across requests doesn't work anyway and this let us show each // await a different set of values. This is important when all awaits // are in third party which would otherwise track all the way to the // internal params. const sharedParamsParent = requestStore.asyncApiPromises.sharedParamsParent const promise: Promise = new Promise((resolve, reject) => { sharedParamsParent.then(() => resolve(underlyingParams), reject) }) // @ts-expect-error promise.displayName = 'params' return instrumentParamsPromiseWithDevWarnings( underlyingParams, promise, workStore ) } const cachedParams = CachedParams.get(underlyingParams) if (cachedParams) { return cachedParams } // We don't use makeResolvedReactPromise here because params // supports copying with spread and we don't want to unnecessarily // instrument the promise with spreadable properties of ReactPromise. const promise = hasFallbackParams ? makeDevtoolsIOAwarePromise( underlyingParams, requestStore, RenderStage.Runtime ) : // We don't want to force an environment transition when this params is not part of the fallback params set Promise.resolve(underlyingParams) const proxiedPromise = instrumentParamsPromiseWithDevWarnings( underlyingParams, promise, workStore ) CachedParams.set(underlyingParams, proxiedPromise) return proxiedPromise } function instrumentParamsPromiseWithDevWarnings( underlyingParams: Params, promise: Promise, workStore: WorkStore ): Promise { // Track which properties we should warn for. const proxiedProperties = new Set() Object.keys(underlyingParams).forEach((prop) => { if (wellKnownProperties.has(prop)) { // These properties cannot be shadowed because they need to be the // true underlying value for Promises to work correctly at runtime } else { proxiedProperties.add(prop) } }) return new Proxy(promise, { get(target, prop, receiver) { if (typeof prop === 'string') { if ( // We are accessing a property that was proxied to the promise instance proxiedProperties.has(prop) ) { const expression = describeStringPropertyAccess('params', prop) warnForSyncAccess(workStore.route, expression) } } return ReflectAdapter.get(target, prop, receiver) }, set(target, prop, value, receiver) { if (typeof prop === 'string') { proxiedProperties.delete(prop) } return ReflectAdapter.set(target, prop, value, receiver) }, ownKeys(target) { const expression = '`...params` or similar expression' warnForSyncAccess(workStore.route, expression) return Reflect.ownKeys(target) }, }) } const warnForSyncAccess = createDedupedByCallsiteServerErrorLoggerDev( createParamsAccessError ) function createParamsAccessError( route: string | undefined, expression: string ) { const prefix = route ? `Route "${route}" ` : 'This route ' return new Error( `${prefix}used ${expression}. ` + `\`params\` is a Promise and must be unwrapped with \`await\` or \`React.use()\` before accessing its properties. ` + `Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis` ) }