next.js/packages/next/src/build/segment-config/app/app-segment-config.ts
app-segment-config.ts293 lines7.7 KB
import { z } from 'next/dist/compiled/zod'
import { formatZodError } from '../../../shared/lib/zod'

const CookieSchema = z
  .object({
    name: z.string(),
    value: z.string().or(z.null()),
  })
  .strict()

const RuntimeSampleSchema = z
  .object({
    cookies: z.array(CookieSchema).optional(),
    headers: z.array(z.tuple([z.string(), z.string().or(z.null())])).optional(),
    params: z.record(z.union([z.string(), z.array(z.string())])).optional(),
    searchParams: z
      .record(z.union([z.string(), z.array(z.string()), z.null()]))
      .optional(),
  })
  .strict()

const InstantConfigObjectSchema = z
  .object({
    samples: z.array(RuntimeSampleSchema).min(1).optional(),
    from: z.array(z.string()).optional(),
    unstable_disableValidation: z.literal(true).optional(),
    unstable_disableDevValidation: z.literal(true).optional(),
    unstable_disableBuildValidation: z.literal(true).optional(),
  })
  .strict()

const InstantConfigSchema = z.union([
  InstantConfigObjectSchema,
  z.literal(true),
  z.literal(false),
])

const PrefetchSchema = z.enum([
  'auto',
  'force-disabled',
  'force-static',
  'force-runtime',
])

export type Instant = InstantConfig | true | false

export type Prefetch =
  | 'auto'
  | 'force-disabled'
  | 'force-static'
  | 'force-runtime'

export type InstantConfigForTypeCheckInternal = __GenericInstantConfig | Instant
// the __GenericInstantConfig type is used to avoid type widening issues with
// our choice to make exports the medium for programming a Next.js application
// With exports the type is controlled by the module and all we can do is assert on it
// from a consumer. However with string literals in objects these are by default typed widely
// and thus cannot match the discriminated union type. If we figure out a better way we should
// delete the __GenericInstantConfig member.
interface __GenericInstantConfig {
  samples?: Array<WideInstantSample>
  from?: string[]
  unstable_disableValidation?: boolean
  unstable_disableDevValidation?: boolean
  unstable_disableBuildValidation?: boolean
}

type WideInstantSample = {
  cookies?: InstantSample['cookies']
  headers?: Array<string[]>
  params?: InstantSample['params']
  searchParams?: InstantSample['searchParams']
}

export interface InstantConfig {
  samples?: Array<InstantSample>
  from?: string[]
  unstable_disableValidation?: true
  unstable_disableDevValidation?: true
  unstable_disableBuildValidation?: true
}

export type InstantSample = {
  cookies?: Array<{
    name: string
    value: string | null
  }>
  headers?: Array<[string, string | null]>
  params?: { [key: string]: string | string[] }
  searchParams?: { [key: string]: string | string[] | null }
}

/**
 * The schema for configuration for a page.
 */
const AppSegmentConfigSchema = z.object({
  /**
   * The number of seconds to revalidate the page or false to disable revalidation.
   */
  revalidate: z
    .union([z.number().int().nonnegative(), z.literal(false)])
    .optional(),

  /**
   * Whether the page supports dynamic parameters.
   */
  dynamicParams: z.boolean().optional(),

  /**
   * The dynamic behavior of the page.
   */
  dynamic: z
    .enum(['auto', 'error', 'force-static', 'force-dynamic'])
    .optional(),

  /**
   * The caching behavior of the page.
   */
  fetchCache: z
    .enum([
      'auto',
      'default-cache',
      'only-cache',
      'force-cache',
      'force-no-store',
      'default-no-store',
      'only-no-store',
    ])
    .optional(),

  /**
   * How this segment should be prefetched.
   */
  unstable_instant: InstantConfigSchema.optional(),

  /**
   * Controls runtime prefetching for this segment.
   * 'static' is a noop (default behavior).
   * 'runtime' enables runtime prefetching.
   */
  unstable_prefetch: PrefetchSchema.optional(),

  /**
   * The stale time for dynamic responses in seconds.
   * Controls how long the client-side router cache retains dynamic page data.
   * Pages only — not allowed in layouts.
   */
  unstable_dynamicStaleTime: z.number().int().nonnegative().optional(),

  /**
   * The preferred region for the page.
   */
  preferredRegion: z.union([z.string(), z.array(z.string())]).optional(),

  /**
   * The runtime to use for the page.
   */
  runtime: z.enum(['edge', 'nodejs']).optional(),

  /**
   * The maximum duration for the page in seconds.
   */
  maxDuration: z.number().int().nonnegative().optional(),
})

/**
 * Parse the app segment config.
 * @param data - The data to parse.
 * @param route - The route of the app.
 * @returns The parsed app segment config.
 */
export function parseAppSegmentConfig(
  data: unknown,
  route: string
): AppSegmentConfig {
  const parsed = AppSegmentConfigSchema.safeParse(data, {
    errorMap: (issue, ctx) => {
      if (issue.path.length === 1) {
        switch (issue.path[0]) {
          case 'revalidate': {
            return {
              message: `Invalid revalidate value ${JSON.stringify(
                ctx.data
              )} on "${route}", must be a non-negative number or false`,
            }
          }
          case 'unstable_instant': {
            return {
              // @TODO replace this link with a link to the docs when they are written
              message: `Invalid unstable_instant value ${JSON.stringify(ctx.data)} on "${route}", must be \`true\`, \`false\`, or an object. Read more at https://nextjs.org/docs/messages/invalid-instant-configuration`,
            }
          }
          case 'unstable_prefetch': {
            return {
              message: `Invalid unstable_prefetch value ${JSON.stringify(ctx.data)} on "${route}", must be "auto", "force-disabled", "force-static", or "force-runtime".`,
            }
          }
          case 'unstable_dynamicStaleTime': {
            return {
              message: `Invalid unstable_dynamicStaleTime value ${JSON.stringify(ctx.data)} on "${route}", must be a non-negative number`,
            }
          }
          default:
        }
      }

      return { message: ctx.defaultError }
    },
  })

  if (!parsed.success) {
    throw formatZodError(
      `Invalid segment configuration options detected for "${route}". Read more at https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config`,
      parsed.error
    )
  }

  return parsed.data
}

/**
 * The configuration for a page.
 */
export type AppSegmentConfig = {
  /**
   * The revalidation period for the page in seconds, or false to disable ISR.
   */
  revalidate?: number | false

  /**
   * Whether the page supports dynamic parameters.
   */
  dynamicParams?: boolean

  /**
   * The dynamic behavior of the page.
   */
  dynamic?: 'auto' | 'error' | 'force-static' | 'force-dynamic'

  /**
   * The caching behavior of the page.
   */
  fetchCache?:
    | 'auto'
    | 'default-cache'
    | 'default-no-store'
    | 'force-cache'
    | 'force-no-store'
    | 'only-cache'
    | 'only-no-store'

  /**
   * How this segment should be prefetched.
   */
  unstable_instant?: Instant

  /**
   * Controls runtime prefetching for this segment.
   * 'static' is a noop (default behavior).
   * 'runtime' enables runtime prefetching.
   */
  unstable_prefetch?: Prefetch

  /**
   * The stale time for dynamic responses in seconds.
   * Controls how long the client-side router cache retains dynamic page data.
   * Pages only — not allowed in layouts.
   */
  unstable_dynamicStaleTime?: number

  /**
   * The preferred region for the page.
   */
  preferredRegion?: string | string[]

  /**
   * The runtime to use for the page.
   */
  runtime?: 'edge' | 'nodejs'

  /**
   * The maximum duration for the page in seconds.
   */
  maxDuration?: number
}

/**
 * The keys of the configuration for a page.
 *
 * @internal - required to exclude zod types from the build
 */
export const AppSegmentConfigSchemaKeys = AppSegmentConfigSchema.keyof().options
Quest for Codev2.0.0
/
SIGN IN