| | import { |
| | getDraftModeProviderForCacheScope, |
| | throwForMissingRequestStore, |
| | } from '../app-render/work-unit-async-storage.external' |
| |
|
| | import type { DraftModeProvider } from '../async-storage/draft-mode-provider' |
| |
|
| | import { |
| | workAsyncStorage, |
| | type WorkStore, |
| | } from '../app-render/work-async-storage.external' |
| | import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external' |
| | import { |
| | abortAndThrowOnSynchronousRequestDataAccess, |
| | delayUntilRuntimeStage, |
| | postponeWithTracking, |
| | trackDynamicDataInDynamicRender, |
| | } from '../app-render/dynamic-rendering' |
| | import { createDedupedByCallsiteServerErrorLoggerDev } from '../create-deduped-by-callsite-server-error-logger' |
| | import { StaticGenBailoutError } from '../../client/components/static-generation-bailout' |
| | import { DynamicServerError } from '../../client/components/hooks-server-context' |
| | import { InvariantError } from '../../shared/lib/invariant-error' |
| | import { ReflectAdapter } from '../web/spec-extension/adapters/reflect' |
| |
|
| | export function draftMode(): Promise<DraftMode> { |
| | const callingExpression = 'draftMode' |
| | const workStore = workAsyncStorage.getStore() |
| | const workUnitStore = workUnitAsyncStorage.getStore() |
| |
|
| | if (!workStore || !workUnitStore) { |
| | throwForMissingRequestStore(callingExpression) |
| | } |
| |
|
| | switch (workUnitStore.type) { |
| | case 'prerender-runtime': |
| | |
| | return delayUntilRuntimeStage( |
| | workUnitStore, |
| | createOrGetCachedDraftMode(workUnitStore.draftMode, workStore) |
| | ) |
| | case 'request': |
| | return createOrGetCachedDraftMode(workUnitStore.draftMode, workStore) |
| |
|
| | case 'cache': |
| | case 'private-cache': |
| | case 'unstable-cache': |
| | |
| | |
| | |
| | const draftModeProvider = getDraftModeProviderForCacheScope( |
| | workStore, |
| | workUnitStore |
| | ) |
| |
|
| | if (draftModeProvider) { |
| | return createOrGetCachedDraftMode(draftModeProvider, workStore) |
| | } |
| |
|
| | |
| | |
| | case 'prerender': |
| | case 'prerender-client': |
| | case 'prerender-ppr': |
| | case 'prerender-legacy': |
| | |
| | return createOrGetCachedDraftMode(null, workStore) |
| |
|
| | default: |
| | return workUnitStore satisfies never |
| | } |
| | } |
| |
|
| | function createOrGetCachedDraftMode( |
| | draftModeProvider: DraftModeProvider | null, |
| | workStore: WorkStore | undefined |
| | ): Promise<DraftMode> { |
| | const cacheKey = draftModeProvider ?? NullDraftMode |
| | const cachedDraftMode = CachedDraftModes.get(cacheKey) |
| |
|
| | if (cachedDraftMode) { |
| | return cachedDraftMode |
| | } |
| |
|
| | if (process.env.NODE_ENV === 'development' && !workStore?.isPrefetchRequest) { |
| | const route = workStore?.route |
| | return createDraftModeWithDevWarnings(draftModeProvider, route) |
| | } else { |
| | return Promise.resolve(new DraftMode(draftModeProvider)) |
| | } |
| | } |
| |
|
| | interface CacheLifetime {} |
| | const NullDraftMode = {} |
| | const CachedDraftModes = new WeakMap<CacheLifetime, Promise<DraftMode>>() |
| |
|
| | function createDraftModeWithDevWarnings( |
| | underlyingProvider: null | DraftModeProvider, |
| | route: undefined | string |
| | ): Promise<DraftMode> { |
| | const instance = new DraftMode(underlyingProvider) |
| | const promise = Promise.resolve(instance) |
| |
|
| | const proxiedPromise = new Proxy(promise, { |
| | get(target, prop, receiver) { |
| | switch (prop) { |
| | case 'isEnabled': |
| | warnForSyncAccess(route, `\`draftMode().${prop}\``) |
| | break |
| | case 'enable': |
| | case 'disable': { |
| | warnForSyncAccess(route, `\`draftMode().${prop}()\``) |
| | break |
| | } |
| | default: { |
| | |
| | } |
| | } |
| |
|
| | return ReflectAdapter.get(target, prop, receiver) |
| | }, |
| | }) |
| |
|
| | return proxiedPromise |
| | } |
| |
|
| | class DraftMode { |
| | |
| | |
| | |
| | private readonly _provider: null | DraftModeProvider |
| |
|
| | constructor(provider: null | DraftModeProvider) { |
| | this._provider = provider |
| | } |
| | get isEnabled() { |
| | if (this._provider !== null) { |
| | return this._provider.isEnabled |
| | } |
| | return false |
| | } |
| | public enable() { |
| | |
| | |
| | trackDynamicDraftMode('draftMode().enable()', this.enable) |
| | if (this._provider !== null) { |
| | this._provider.enable() |
| | } |
| | } |
| | public disable() { |
| | trackDynamicDraftMode('draftMode().disable()', this.disable) |
| | if (this._provider !== null) { |
| | this._provider.disable() |
| | } |
| | } |
| | } |
| | const warnForSyncAccess = createDedupedByCallsiteServerErrorLoggerDev( |
| | createDraftModeAccessError |
| | ) |
| |
|
| | function createDraftModeAccessError( |
| | route: string | undefined, |
| | expression: string |
| | ) { |
| | const prefix = route ? `Route "${route}" ` : 'This route ' |
| | return new Error( |
| | `${prefix}used ${expression}. ` + |
| | `\`draftMode()\` returns 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` |
| | ) |
| | } |
| |
|
| | function trackDynamicDraftMode(expression: string, constructorOpt: Function) { |
| | const workStore = workAsyncStorage.getStore() |
| | const workUnitStore = workUnitAsyncStorage.getStore() |
| |
|
| | if (workStore) { |
| | |
| | |
| | if (workUnitStore?.phase === 'after') { |
| | throw new Error( |
| | `Route ${workStore.route} used "${expression}" inside \`after()\`. The enabled status of \`draftMode()\` can be read inside \`after()\` but you cannot enable or disable \`draftMode()\`. See more info here: https://nextjs.org/docs/app/api-reference/functions/after` |
| | ) |
| | } |
| |
|
| | if (workStore.dynamicShouldError) { |
| | throw new StaticGenBailoutError( |
| | `Route ${workStore.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` |
| | ) |
| | } |
| |
|
| | if (workUnitStore) { |
| | switch (workUnitStore.type) { |
| | case 'cache': |
| | case 'private-cache': { |
| | const error = new Error( |
| | `Route ${workStore.route} used "${expression}" inside "use cache". The enabled status of \`draftMode()\` can be read in caches but you must not enable or disable \`draftMode()\` inside a cache. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache` |
| | ) |
| | Error.captureStackTrace(error, constructorOpt) |
| | workStore.invalidDynamicUsageError ??= error |
| | throw error |
| | } |
| | case 'unstable-cache': |
| | throw new Error( |
| | `Route ${workStore.route} used "${expression}" inside a function cached with \`unstable_cache()\`. The enabled status of \`draftMode()\` can be read in caches but you must not enable or disable \`draftMode()\` inside a cache. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache` |
| | ) |
| |
|
| | case 'prerender': |
| | case 'prerender-runtime': { |
| | const error = new Error( |
| | `Route ${workStore.route} used ${expression} without first calling \`await connection()\`. See more info here: https://nextjs.org/docs/messages/next-prerender-sync-headers` |
| | ) |
| | return abortAndThrowOnSynchronousRequestDataAccess( |
| | workStore.route, |
| | expression, |
| | error, |
| | workUnitStore |
| | ) |
| | } |
| | case 'prerender-client': |
| | const exportName = '`draftMode`' |
| | throw new InvariantError( |
| | `${exportName} must not be used within a Client Component. Next.js should be preventing ${exportName} from being included in Client Components statically, but did not in this case.` |
| | ) |
| | case 'prerender-ppr': |
| | return postponeWithTracking( |
| | workStore.route, |
| | expression, |
| | workUnitStore.dynamicTracking |
| | ) |
| | case 'prerender-legacy': |
| | workUnitStore.revalidate = 0 |
| |
|
| | const err = new DynamicServerError( |
| | `Route ${workStore.route} couldn't be rendered statically because it used \`${expression}\`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error` |
| | ) |
| | workStore.dynamicUsageDescription = expression |
| | workStore.dynamicUsageStack = err.stack |
| |
|
| | throw err |
| | case 'request': |
| | trackDynamicDataInDynamicRender(workUnitStore) |
| | break |
| | default: |
| | workUnitStore satisfies never |
| | } |
| | } |
| | } |
| | } |
| |
|