next.js/packages/next/src/server/request/draft-mode.ts
draft-mode.ts266 lines10.0 KB
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,
  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 { delayUntilRuntimeStage } from '../dynamic-rendering-utils'
import { ReflectAdapter } from '../web/spec-extension/adapters/reflect'
import { applyOwnerStack } from '../dynamic-rendering-utils'

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':
      // TODO(runtime-ppr): does it make sense to delay this? normally it's always microtasky
      return delayUntilRuntimeStage(
        workUnitStore,
        createOrGetCachedDraftMode(workUnitStore.draftMode, workStore)
      )
    case 'request':
      return createOrGetCachedDraftMode(workUnitStore.draftMode, workStore)

    case 'cache':
    case 'private-cache':
    case 'unstable-cache':
      // Inside of `"use cache"` or `unstable_cache`, draft mode is available if
      // the outmost work unit store is a request store (or a runtime prerender),
      // and if draft mode is enabled.
      const draftModeProvider = getDraftModeProviderForCacheScope(
        workStore,
        workUnitStore
      )

      if (draftModeProvider) {
        return createOrGetCachedDraftMode(draftModeProvider, workStore)
      }

    // Otherwise, we fall through to providing an empty draft mode.
    // eslint-disable-next-line no-fallthrough
    case 'prerender':
    case 'prerender-ppr':
    case 'prerender-legacy':
      // Return empty draft mode
      return createOrGetCachedDraftMode(null, workStore)
    case 'prerender-client':
    case 'validation-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 'generate-static-params':
      throw new Error(
        `Route ${workStore.route} used \`${callingExpression}()\` inside \`generateStaticParams\`. This is not supported because \`generateStaticParams\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context`
      )

    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: {
          // We only warn for well-defined properties of the draftMode object.
        }
      }

      return ReflectAdapter.get(target, prop, receiver)
    },
  })

  return proxiedPromise
}

class DraftMode {
  /**
   * @internal - this declaration is stripped via `tsc --stripInternal`
   */
  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() {
    // We have a store we want to track dynamic data access to ensure we
    // don't statically generate routes that manipulate draft mode.
    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) {
    // We have a store we want to track dynamic data access to ensure we
    // don't statically generate routes that manipulate draft mode.
    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)
          applyOwnerStack(error)
          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':
        case 'validation-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
        case 'generate-static-params':
          throw new Error(
            `Route ${workStore.route} used \`${expression}\` inside \`generateStaticParams\`. This is not supported because \`generateStaticParams\` runs at build time without an HTTP request. Read more: https://nextjs.org/docs/messages/next-dynamic-api-wrong-context`
          )
        default:
          workUnitStore satisfies never
      }
    }
  }
}
Quest for Codev2.0.0
/
SIGN IN