| | import type { WorkStore } from '../app-render/work-async-storage.external' |
| |
|
| | import { ReflectAdapter } from '../web/spec-extension/adapters/reflect' |
| | import { |
| | throwToInterruptStaticGeneration, |
| | postponeWithTracking, |
| | annotateDynamicAccess, |
| | delayUntilRuntimeStage, |
| | } from '../app-render/dynamic-rendering' |
| |
|
| | import { |
| | workUnitAsyncStorage, |
| | type PrerenderStoreLegacy, |
| | type PrerenderStorePPR, |
| | type PrerenderStoreModern, |
| | type PrerenderStoreModernRuntime, |
| | type StaticPrerenderStore, |
| | throwInvariantForMissingStore, |
| | type RequestStore, |
| | } from '../app-render/work-unit-async-storage.external' |
| | import { InvariantError } from '../../shared/lib/invariant-error' |
| | import { |
| | makeDevtoolsIOAwarePromise, |
| | makeHangingPromise, |
| | } from '../dynamic-rendering-utils' |
| | import { createDedupedByCallsiteServerErrorLoggerDev } from '../create-deduped-by-callsite-server-error-logger' |
| | import { |
| | describeStringPropertyAccess, |
| | describeHasCheckingStringProperty, |
| | wellKnownProperties, |
| | } from '../../shared/lib/utils/reflect-utils' |
| | import { |
| | throwWithStaticGenerationBailoutErrorWithDynamicError, |
| | throwForSearchParamsAccessInUseCache, |
| | } from './utils' |
| | import { RenderStage } from '../app-render/staged-rendering' |
| |
|
| | export type SearchParams = { [key: string]: string | string[] | undefined } |
| |
|
| | export function createSearchParamsFromClient( |
| | underlyingSearchParams: SearchParams, |
| | workStore: WorkStore |
| | ): Promise<SearchParams> { |
| | const workUnitStore = workUnitAsyncStorage.getStore() |
| | if (workUnitStore) { |
| | switch (workUnitStore.type) { |
| | case 'prerender': |
| | case 'prerender-client': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | return createStaticPrerenderSearchParams(workStore, workUnitStore) |
| | case 'prerender-runtime': |
| | throw new InvariantError( |
| | 'createSearchParamsFromClient should not be called in a runtime prerender.' |
| | ) |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | throw new InvariantError( |
| | 'createSearchParamsFromClient should not be called in cache contexts.' |
| | ) |
| | case 'request': |
| | return createRenderSearchParams( |
| | underlyingSearchParams, |
| | workStore, |
| | workUnitStore |
| | ) |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| | throwInvariantForMissingStore() |
| | } |
| |
|
| | |
| | export const createServerSearchParamsForMetadata = |
| | createServerSearchParamsForServerPage |
| |
|
| | export function createServerSearchParamsForServerPage( |
| | underlyingSearchParams: SearchParams, |
| | workStore: WorkStore |
| | ): Promise<SearchParams> { |
| | const workUnitStore = workUnitAsyncStorage.getStore() |
| | if (workUnitStore) { |
| | switch (workUnitStore.type) { |
| | case 'prerender': |
| | case 'prerender-client': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | return createStaticPrerenderSearchParams(workStore, workUnitStore) |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | throw new InvariantError( |
| | 'createServerSearchParamsForServerPage should not be called in cache contexts.' |
| | ) |
| | case 'prerender-runtime': |
| | return createRuntimePrerenderSearchParams( |
| | underlyingSearchParams, |
| | workUnitStore |
| | ) |
| | case 'request': |
| | return createRenderSearchParams( |
| | underlyingSearchParams, |
| | workStore, |
| | workUnitStore |
| | ) |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| | throwInvariantForMissingStore() |
| | } |
| |
|
| | export function createPrerenderSearchParamsForClientPage( |
| | workStore: WorkStore |
| | ): Promise<SearchParams> { |
| | if (workStore.forceStatic) { |
| | |
| | |
| | return Promise.resolve({}) |
| | } |
| |
|
| | const workUnitStore = workUnitAsyncStorage.getStore() |
| | if (workUnitStore) { |
| | switch (workUnitStore.type) { |
| | case 'prerender': |
| | case 'prerender-client': |
| | |
| | |
| | return makeHangingPromise( |
| | workUnitStore.renderSignal, |
| | workStore.route, |
| | '`searchParams`' |
| | ) |
| | case 'prerender-runtime': |
| | throw new InvariantError( |
| | 'createPrerenderSearchParamsForClientPage should not be called in a runtime prerender.' |
| | ) |
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | throw new InvariantError( |
| | 'createPrerenderSearchParamsForClientPage should not be called in cache contexts.' |
| | ) |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | case 'request': |
| | return Promise.resolve({}) |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| | throwInvariantForMissingStore() |
| | } |
| |
|
| | function createStaticPrerenderSearchParams( |
| | workStore: WorkStore, |
| | prerenderStore: StaticPrerenderStore |
| | ): Promise<SearchParams> { |
| | if (workStore.forceStatic) { |
| | |
| | |
| | return Promise.resolve({}) |
| | } |
| |
|
| | switch (prerenderStore.type) { |
| | case 'prerender': |
| | case 'prerender-client': |
| | |
| | return makeHangingSearchParams(workStore, prerenderStore) |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | |
| | |
| | return makeErroringSearchParams(workStore, prerenderStore) |
| | default: |
| | return prerenderStore satisfies never |
| | } |
| | } |
| |
|
| | function createRuntimePrerenderSearchParams( |
| | underlyingSearchParams: SearchParams, |
| | workUnitStore: PrerenderStoreModernRuntime |
| | ): Promise<SearchParams> { |
| | return delayUntilRuntimeStage( |
| | workUnitStore, |
| | makeUntrackedSearchParams(underlyingSearchParams) |
| | ) |
| | } |
| |
|
| | function createRenderSearchParams( |
| | underlyingSearchParams: SearchParams, |
| | workStore: WorkStore, |
| | requestStore: RequestStore |
| | ): Promise<SearchParams> { |
| | if (workStore.forceStatic) { |
| | |
| | |
| | return Promise.resolve({}) |
| | } else { |
| | if (process.env.NODE_ENV === 'development') { |
| | |
| | |
| | |
| | return makeUntrackedSearchParamsWithDevWarnings( |
| | underlyingSearchParams, |
| | workStore, |
| | requestStore |
| | ) |
| | } else { |
| | return makeUntrackedSearchParams(underlyingSearchParams) |
| | } |
| | } |
| | } |
| |
|
| | interface CacheLifetime {} |
| | const CachedSearchParams = new WeakMap<CacheLifetime, Promise<SearchParams>>() |
| |
|
| | const CachedSearchParamsForUseCache = new WeakMap< |
| | CacheLifetime, |
| | Promise<SearchParams> |
| | >() |
| |
|
| | function makeHangingSearchParams( |
| | workStore: WorkStore, |
| | prerenderStore: PrerenderStoreModern |
| | ): Promise<SearchParams> { |
| | const cachedSearchParams = CachedSearchParams.get(prerenderStore) |
| | if (cachedSearchParams) { |
| | return cachedSearchParams |
| | } |
| |
|
| | const promise = makeHangingPromise<SearchParams>( |
| | prerenderStore.renderSignal, |
| | workStore.route, |
| | '`searchParams`' |
| | ) |
| |
|
| | const proxiedPromise = new Proxy(promise, { |
| | get(target, prop, receiver) { |
| | if (Object.hasOwn(promise, prop)) { |
| | |
| | |
| | |
| | return ReflectAdapter.get(target, prop, receiver) |
| | } |
| |
|
| | switch (prop) { |
| | case 'then': { |
| | const expression = |
| | '`await searchParams`, `searchParams.then`, or similar' |
| | annotateDynamicAccess(expression, prerenderStore) |
| | return ReflectAdapter.get(target, prop, receiver) |
| | } |
| | case 'status': { |
| | const expression = |
| | '`use(searchParams)`, `searchParams.status`, or similar' |
| | annotateDynamicAccess(expression, prerenderStore) |
| | return ReflectAdapter.get(target, prop, receiver) |
| | } |
| |
|
| | default: { |
| | return ReflectAdapter.get(target, prop, receiver) |
| | } |
| | } |
| | }, |
| | }) |
| |
|
| | CachedSearchParams.set(prerenderStore, proxiedPromise) |
| | return proxiedPromise |
| | } |
| |
|
| | function makeErroringSearchParams( |
| | workStore: WorkStore, |
| | prerenderStore: PrerenderStoreLegacy | PrerenderStorePPR |
| | ): Promise<SearchParams> { |
| | const cachedSearchParams = CachedSearchParams.get(workStore) |
| | if (cachedSearchParams) { |
| | return cachedSearchParams |
| | } |
| |
|
| | const underlyingSearchParams = {} |
| | |
| | |
| | |
| | const promise = Promise.resolve(underlyingSearchParams) |
| |
|
| | const proxiedPromise = new Proxy(promise, { |
| | get(target, prop, receiver) { |
| | if (Object.hasOwn(promise, prop)) { |
| | |
| | |
| | |
| | return ReflectAdapter.get(target, prop, receiver) |
| | } |
| |
|
| | if (typeof prop === 'string' && prop === 'then') { |
| | const expression = |
| | '`await searchParams`, `searchParams.then`, or similar' |
| | if (workStore.dynamicShouldError) { |
| | throwWithStaticGenerationBailoutErrorWithDynamicError( |
| | workStore.route, |
| | expression |
| | ) |
| | } else if (prerenderStore.type === 'prerender-ppr') { |
| | |
| | postponeWithTracking( |
| | workStore.route, |
| | expression, |
| | prerenderStore.dynamicTracking |
| | ) |
| | } else { |
| | |
| | throwToInterruptStaticGeneration( |
| | expression, |
| | workStore, |
| | prerenderStore |
| | ) |
| | } |
| | } |
| | return ReflectAdapter.get(target, prop, receiver) |
| | }, |
| | }) |
| |
|
| | CachedSearchParams.set(workStore, proxiedPromise) |
| | return proxiedPromise |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | export function makeErroringSearchParamsForUseCache( |
| | workStore: WorkStore |
| | ): Promise<SearchParams> { |
| | const cachedSearchParams = CachedSearchParamsForUseCache.get(workStore) |
| | if (cachedSearchParams) { |
| | return cachedSearchParams |
| | } |
| |
|
| | const promise = Promise.resolve({}) |
| |
|
| | const proxiedPromise = new Proxy(promise, { |
| | get: function get(target, prop, receiver) { |
| | if (Object.hasOwn(promise, prop)) { |
| | |
| | |
| | |
| | |
| | return ReflectAdapter.get(target, prop, receiver) |
| | } |
| |
|
| | if ( |
| | typeof prop === 'string' && |
| | (prop === 'then' || !wellKnownProperties.has(prop)) |
| | ) { |
| | throwForSearchParamsAccessInUseCache(workStore, get) |
| | } |
| |
|
| | return ReflectAdapter.get(target, prop, receiver) |
| | }, |
| | }) |
| |
|
| | CachedSearchParamsForUseCache.set(workStore, proxiedPromise) |
| | return proxiedPromise |
| | } |
| |
|
| | function makeUntrackedSearchParams( |
| | underlyingSearchParams: SearchParams |
| | ): Promise<SearchParams> { |
| | const cachedSearchParams = CachedSearchParams.get(underlyingSearchParams) |
| | if (cachedSearchParams) { |
| | return cachedSearchParams |
| | } |
| |
|
| | const promise = Promise.resolve(underlyingSearchParams) |
| | CachedSearchParams.set(underlyingSearchParams, promise) |
| |
|
| | return promise |
| | } |
| |
|
| | function makeUntrackedSearchParamsWithDevWarnings( |
| | underlyingSearchParams: SearchParams, |
| | workStore: WorkStore, |
| | requestStore: RequestStore |
| | ): Promise<SearchParams> { |
| | if (requestStore.asyncApiPromises) { |
| | |
| | |
| | return makeUntrackedSearchParamsWithDevWarningsImpl( |
| | underlyingSearchParams, |
| | workStore, |
| | requestStore |
| | ) |
| | } else { |
| | const cachedSearchParams = CachedSearchParams.get(underlyingSearchParams) |
| | if (cachedSearchParams) { |
| | return cachedSearchParams |
| | } |
| | const promise = makeUntrackedSearchParamsWithDevWarningsImpl( |
| | underlyingSearchParams, |
| | workStore, |
| | requestStore |
| | ) |
| | CachedSearchParams.set(requestStore, promise) |
| | return promise |
| | } |
| | } |
| |
|
| | function makeUntrackedSearchParamsWithDevWarningsImpl( |
| | underlyingSearchParams: SearchParams, |
| | workStore: WorkStore, |
| | requestStore: RequestStore |
| | ): Promise<SearchParams> { |
| | const promiseInitialized = { current: false } |
| | const proxiedUnderlying = instrumentSearchParamsObjectWithDevWarnings( |
| | underlyingSearchParams, |
| | workStore, |
| | promiseInitialized |
| | ) |
| |
|
| | let promise: Promise<SearchParams> |
| | if (requestStore.asyncApiPromises) { |
| | |
| | |
| | |
| | const sharedSearchParamsParent = |
| | requestStore.asyncApiPromises.sharedSearchParamsParent |
| | promise = new Promise((resolve, reject) => { |
| | sharedSearchParamsParent.then(() => resolve(proxiedUnderlying), reject) |
| | }) |
| | |
| | promise.displayName = 'searchParams' |
| | } else { |
| | promise = makeDevtoolsIOAwarePromise( |
| | proxiedUnderlying, |
| | requestStore, |
| | RenderStage.Runtime |
| | ) |
| | } |
| | promise.then( |
| | () => { |
| | promiseInitialized.current = true |
| | }, |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | ignoreReject |
| | ) |
| |
|
| | return instrumentSearchParamsPromiseWithDevWarnings( |
| | underlyingSearchParams, |
| | promise, |
| | workStore |
| | ) |
| | } |
| |
|
| | function ignoreReject() {} |
| |
|
| | function instrumentSearchParamsObjectWithDevWarnings( |
| | underlyingSearchParams: SearchParams, |
| | workStore: WorkStore, |
| | promiseInitialized: { current: boolean } |
| | ) { |
| | |
| | |
| | |
| | |
| | |
| | |
| | return new Proxy(underlyingSearchParams, { |
| | get(target, prop, receiver) { |
| | if (typeof prop === 'string' && promiseInitialized.current) { |
| | if (workStore.dynamicShouldError) { |
| | const expression = describeStringPropertyAccess('searchParams', prop) |
| | throwWithStaticGenerationBailoutErrorWithDynamicError( |
| | workStore.route, |
| | expression |
| | ) |
| | } |
| | } |
| | return ReflectAdapter.get(target, prop, receiver) |
| | }, |
| | has(target, prop) { |
| | if (typeof prop === 'string') { |
| | if (workStore.dynamicShouldError) { |
| | const expression = describeHasCheckingStringProperty( |
| | 'searchParams', |
| | prop |
| | ) |
| | throwWithStaticGenerationBailoutErrorWithDynamicError( |
| | workStore.route, |
| | expression |
| | ) |
| | } |
| | } |
| | return Reflect.has(target, prop) |
| | }, |
| | ownKeys(target) { |
| | if (workStore.dynamicShouldError) { |
| | const expression = |
| | '`{...searchParams}`, `Object.keys(searchParams)`, or similar' |
| | throwWithStaticGenerationBailoutErrorWithDynamicError( |
| | workStore.route, |
| | expression |
| | ) |
| | } |
| | return Reflect.ownKeys(target) |
| | }, |
| | }) |
| | } |
| |
|
| | function instrumentSearchParamsPromiseWithDevWarnings( |
| | underlyingSearchParams: SearchParams, |
| | promise: Promise<SearchParams>, |
| | workStore: WorkStore |
| | ) { |
| | |
| | const proxiedProperties = new Set<string>() |
| |
|
| | Object.keys(underlyingSearchParams).forEach((prop) => { |
| | if (wellKnownProperties.has(prop)) { |
| | |
| | |
| | } else { |
| | proxiedProperties.add(prop) |
| | } |
| | }) |
| |
|
| | return new Proxy(promise, { |
| | get(target, prop, receiver) { |
| | if (prop === 'then' && workStore.dynamicShouldError) { |
| | const expression = '`searchParams.then`' |
| | throwWithStaticGenerationBailoutErrorWithDynamicError( |
| | workStore.route, |
| | expression |
| | ) |
| | } |
| | if (typeof prop === 'string') { |
| | if ( |
| | !wellKnownProperties.has(prop) && |
| | (proxiedProperties.has(prop) || |
| | |
| | |
| | Reflect.has(target, prop) === false) |
| | ) { |
| | const expression = describeStringPropertyAccess('searchParams', prop) |
| | warnForSyncAccess(workStore.route, expression) |
| | } |
| | } |
| | return ReflectAdapter.get(target, prop, receiver) |
| | }, |
| | set(target, prop, value, receiver) { |
| | if (typeof prop === 'string') { |
| | proxiedProperties.delete(prop) |
| | } |
| | return Reflect.set(target, prop, value, receiver) |
| | }, |
| | has(target, prop) { |
| | if (typeof prop === 'string') { |
| | if ( |
| | !wellKnownProperties.has(prop) && |
| | (proxiedProperties.has(prop) || |
| | |
| | |
| | Reflect.has(target, prop) === false) |
| | ) { |
| | const expression = describeHasCheckingStringProperty( |
| | 'searchParams', |
| | prop |
| | ) |
| | warnForSyncAccess(workStore.route, expression) |
| | } |
| | } |
| | return Reflect.has(target, prop) |
| | }, |
| | ownKeys(target) { |
| | const expression = '`Object.keys(searchParams)` or similar' |
| | warnForSyncAccess(workStore.route, expression) |
| | return Reflect.ownKeys(target) |
| | }, |
| | }) |
| | } |
| |
|
| | const warnForSyncAccess = createDedupedByCallsiteServerErrorLoggerDev( |
| | createSearchAccessError |
| | ) |
| |
|
| | function createSearchAccessError( |
| | route: string | undefined, |
| | expression: string |
| | ) { |
| | const prefix = route ? `Route "${route}" ` : 'This route ' |
| | return new Error( |
| | `${prefix}used ${expression}. ` + |
| | `\`searchParams\` 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` |
| | ) |
| | } |
| |
|