next.js/packages/next/src/build/analysis/get-page-static-info.ts
get-page-static-info.ts884 lines26.6 KB
import type { NextConfig } from '../../server/config-shared'
import type { RouteHas } from '../../lib/load-custom-routes'

import { readFileSync } from 'fs'
import { relative } from 'path'
import { LRUCache } from '../../server/lib/lru-cache'
import { extractExportedConstValue } from './extract-const-value'
import { parseModule } from './parse-module'
import * as Log from '../output/log'
import {
  SERVER_RUNTIME,
  MIDDLEWARE_FILENAME,
  PROXY_FILENAME,
} from '../../lib/constants'
import { tryToParsePath } from '../../lib/try-to-parse-path'
import { isAPIRoute } from '../../lib/is-api-route'
import { isEdgeRuntime } from '../../lib/is-edge-runtime'
import {
  warnAboutEdgeRuntime,
  warnAboutPreferredRegion,
} from '../warn-about-edge-runtime'
import { RSC_MODULE_TYPES } from '../../shared/lib/constants'
import type { RSCMeta } from '../webpack/loaders/get-module-build-info'
import { PAGE_TYPES } from '../../lib/page-types'
import {
  AppSegmentConfigSchemaKeys,
  parseAppSegmentConfig,
  type AppSegmentConfig,
} from '../segment-config/app/app-segment-config'
import { reportZodError } from '../../shared/lib/zod'
import {
  PagesSegmentConfigSchemaKeys,
  parsePagesSegmentConfig,
  type PagesSegmentConfig,
  type PagesSegmentConfigConfig,
} from '../segment-config/pages/pages-segment-config'
import {
  MiddlewareConfigInputSchema,
  SourceSchema,
  type MiddlewareConfigMatcherInput,
} from '../segment-config/middleware/middleware-config'
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path'
import { isProxyFile } from '../utils'

const PARSE_PATTERN =
  /(?<!(_jsx|jsx-))runtime|preferredRegion|getStaticProps|getServerSideProps|generateStaticParams|export const|generateImageMetadata|generateSitemaps|middleware|proxy/

export type ProxyMatcher = {
  regexp: string
  locale?: false
  has?: RouteHas[]
  missing?: RouteHas[]
  originalSource: string
}

export type ProxyConfig = {
  /**
   * The matcher for the proxy. Read more: [Next.js Docs: Proxy `matcher`](https://nextjs.org/docs/app/api-reference/file-conventions/proxy#matcher),
   * [Next.js Docs: Proxy matching paths](https://nextjs.org/docs/app/building-your-application/routing/proxy#matching-paths)
   */
  matchers?: ProxyMatcher[]

  /**
   * The regions that the proxy should run in.
   */
  regions?: string | string[]

  /**
   * A glob, or an array of globs, ignoring dynamic code evaluation for specific
   * files. The globs are relative to your application root folder.
   */
  unstable_allowDynamic?: string[]
}

export interface AppPageStaticInfo {
  type: PAGE_TYPES.APP
  ssg?: boolean
  ssr?: boolean
  rsc?: RSCModuleType
  generateStaticParams?: boolean
  generateSitemaps?: boolean
  generateImageMetadata?: boolean
  middleware?: ProxyConfig
  config: Omit<AppSegmentConfig, 'runtime' | 'maxDuration'> | undefined
  runtime: AppSegmentConfig['runtime'] | undefined
  preferredRegion: AppSegmentConfig['preferredRegion'] | undefined
  maxDuration: number | undefined
  hadUnsupportedValue: boolean
}

export interface PagesPageStaticInfo {
  type: PAGE_TYPES.PAGES
  getStaticProps?: boolean
  getServerSideProps?: boolean
  rsc?: RSCModuleType
  generateStaticParams?: boolean
  generateSitemaps?: boolean
  generateImageMetadata?: boolean
  middleware?: ProxyConfig
  config:
    | (Omit<PagesSegmentConfig, 'runtime' | 'config' | 'maxDuration'> & {
        config?: Omit<PagesSegmentConfigConfig, 'runtime' | 'maxDuration'>
      })
    | undefined
  runtime: PagesSegmentConfig['runtime'] | undefined
  preferredRegion: PagesSegmentConfigConfig['regions'] | undefined
  maxDuration: number | undefined
  hadUnsupportedValue: boolean
}

export type PageStaticInfo = AppPageStaticInfo | PagesPageStaticInfo

const CLIENT_MODULE_LABEL =
  /\/\* __next_internal_client_entry_do_not_use__ ([^ ]*) (cjs|auto) \*\//

// Match JSON object that may contain nested objects (for loc info)
// The JSON ends right before the closing " */"
const ACTION_MODULE_LABEL =
  /\/\* __next_internal_action_entry_do_not_use__ (\{.*\}) \*\//

const CLIENT_DIRECTIVE = 'use client'
const SERVER_ACTION_DIRECTIVE = 'use server'

export type RSCModuleType = 'server' | 'client'
export function getRSCModuleInformation(
  source: string,
  isReactServerLayer: boolean
): RSCMeta {
  const actionsJson = source.match(ACTION_MODULE_LABEL)
  // Parse action metadata - supports both old format (string) and new format (object with loc)
  const parsedActionsMeta = actionsJson
    ? (JSON.parse(actionsJson[1]) as RSCMeta['actionIds'])
    : undefined
  const clientInfoMatch = source.match(CLIENT_MODULE_LABEL)
  const isClientRef = !!clientInfoMatch

  if (!isReactServerLayer) {
    return {
      type: RSC_MODULE_TYPES.client,
      actionIds: parsedActionsMeta,
      isClientRef,
    }
  }

  const clientRefsString = clientInfoMatch?.[1]
  const clientRefs = clientRefsString ? clientRefsString.split(',') : []
  const clientEntryType = clientInfoMatch?.[2] as 'cjs' | 'auto' | undefined

  const type = clientInfoMatch
    ? RSC_MODULE_TYPES.client
    : RSC_MODULE_TYPES.server

  return {
    type,
    actionIds: parsedActionsMeta,
    clientRefs,
    clientEntryType,
    isClientRef,
  }
}

/**
 * Receives a parsed AST from SWC and checks if it belongs to a module that
 * requires a runtime to be specified. Those are:
 *   - Modules with `export function getStaticProps | getServerSideProps`
 *   - Modules with `export { getStaticProps | getServerSideProps } <from ...>`
 *   - Modules with `export const runtime = ...`
 */
function checkExports(
  ast: any,
  expectedExports: string[],
  page: string
): {
  getStaticProps?: boolean
  getServerSideProps?: boolean
  generateImageMetadata?: boolean
  generateSitemaps?: boolean
  generateStaticParams?: boolean
  directives?: Set<string>
  exports?: Set<string>
} {
  const exportsSet = new Set<string>([
    'getStaticProps',
    'getServerSideProps',
    'generateImageMetadata',
    'generateSitemaps',
    'generateStaticParams',
  ])
  if (!Array.isArray(ast?.body)) {
    return {}
  }

  try {
    let getStaticProps: boolean = false
    let getServerSideProps: boolean = false
    let generateImageMetadata: boolean = false
    let generateSitemaps: boolean = false
    let generateStaticParams = false
    let exports = new Set<string>()
    let directives = new Set<string>()
    let hasLeadingNonDirectiveNode = false

    for (const node of ast.body) {
      // There should be no non-string literals nodes before directives
      if (
        node.type === 'ExpressionStatement' &&
        node.expression.type === 'StringLiteral'
      ) {
        if (!hasLeadingNonDirectiveNode) {
          const directive = node.expression.value
          if (CLIENT_DIRECTIVE === directive) {
            directives.add('client')
          }
          if (SERVER_ACTION_DIRECTIVE === directive) {
            directives.add('server')
          }
        }
      } else {
        hasLeadingNonDirectiveNode = true
      }
      if (
        node.type === 'ExportDeclaration' &&
        node.declaration?.type === 'VariableDeclaration'
      ) {
        for (const declaration of node.declaration?.declarations) {
          if (expectedExports.includes(declaration.id.value)) {
            exports.add(declaration.id.value)
          }
        }
      }

      if (
        node.type === 'ExportDeclaration' &&
        node.declaration?.type === 'FunctionDeclaration' &&
        exportsSet.has(node.declaration.identifier?.value)
      ) {
        const id = node.declaration.identifier.value
        getServerSideProps = id === 'getServerSideProps'
        getStaticProps = id === 'getStaticProps'
        generateImageMetadata = id === 'generateImageMetadata'
        generateSitemaps = id === 'generateSitemaps'
        generateStaticParams = id === 'generateStaticParams'
      }

      if (
        node.type === 'ExportDeclaration' &&
        node.declaration?.type === 'VariableDeclaration'
      ) {
        const id = node.declaration?.declarations[0]?.id.value
        if (exportsSet.has(id)) {
          getServerSideProps = id === 'getServerSideProps'
          getStaticProps = id === 'getStaticProps'
          generateImageMetadata = id === 'generateImageMetadata'
          generateSitemaps = id === 'generateSitemaps'
          generateStaticParams = id === 'generateStaticParams'
        }
      }

      if (node.type === 'ExportNamedDeclaration') {
        for (const specifier of node.specifiers) {
          if (
            specifier.type === 'ExportSpecifier' &&
            specifier.orig?.type === 'Identifier'
          ) {
            const value = specifier.orig.value

            if (!getServerSideProps && value === 'getServerSideProps') {
              getServerSideProps = true
            }
            if (!getStaticProps && value === 'getStaticProps') {
              getStaticProps = true
            }
            if (!generateImageMetadata && value === 'generateImageMetadata') {
              generateImageMetadata = true
            }
            if (!generateSitemaps && value === 'generateSitemaps') {
              generateSitemaps = true
            }
            if (!generateStaticParams && value === 'generateStaticParams') {
              generateStaticParams = true
            }
            if (expectedExports.includes(value) && !exports.has(value)) {
              // An export was found that was actually a re-export, and not a
              // literal value. We should warn here.
              Log.warn(
                `Next.js can't recognize the exported \`${value}\` field in "${page}", it may be re-exported from another file. The default config will be used instead.`
              )
            }
          }
        }
      }
    }

    return {
      getStaticProps,
      getServerSideProps,
      generateImageMetadata,
      generateSitemaps,
      generateStaticParams,
      directives,
      exports,
    }
  } catch {}

  return {}
}

function validateMiddlewareProxyExports({
  ast,
  page,
  pageFilePath,
  isDev,
}: {
  ast: any
  page: string
  pageFilePath: string
  isDev: boolean
}): void {
  // Check if this is middleware/proxy
  const isMiddleware =
    page === `/${MIDDLEWARE_FILENAME}` || page === `/src/${MIDDLEWARE_FILENAME}`
  const isProxy =
    page === `/${PROXY_FILENAME}` || page === `/src/${PROXY_FILENAME}`

  if (!isMiddleware && !isProxy) {
    return
  }

  if (!ast || !Array.isArray(ast.body)) {
    return
  }

  const fileName = isProxy ? PROXY_FILENAME : MIDDLEWARE_FILENAME

  // Parse AST to get export info (since checkExports doesn't return middleware/proxy info)
  let hasDefaultExport = false
  let hasMiddlewareExport = false
  let hasProxyExport = false

  for (const node of ast.body) {
    if (
      node.type === 'ExportDefaultDeclaration' ||
      node.type === 'ExportDefaultExpression'
    ) {
      hasDefaultExport = true
    }

    if (
      node.type === 'ExportDeclaration' &&
      node.declaration?.type === 'FunctionDeclaration'
    ) {
      const id = node.declaration.identifier?.value
      if (id === 'middleware') {
        hasMiddlewareExport = true
      }
      if (id === 'proxy') {
        hasProxyExport = true
      }
    }

    if (
      node.type === 'ExportDeclaration' &&
      node.declaration?.type === 'VariableDeclaration'
    ) {
      const id = node.declaration?.declarations[0]?.id.value
      if (id === 'middleware') {
        hasMiddlewareExport = true
      }
      if (id === 'proxy') {
        hasProxyExport = true
      }
    }

    if (node.type === 'ExportNamedDeclaration') {
      for (const specifier of node.specifiers) {
        if (
          specifier.type === 'ExportSpecifier' &&
          specifier.orig?.type === 'Identifier'
        ) {
          // Use the exported name if it exists (for aliased exports like `export { foo as proxy }`),
          // otherwise fall back to the original name (for simple re-exports like `export { proxy }`)
          const exportedIdentifier = specifier.exported || specifier.orig
          const value = exportedIdentifier.value
          if (value === 'middleware') {
            hasMiddlewareExport = true
          }
          if (value === 'proxy') {
            hasProxyExport = true
          }
        }
      }
    }
  }

  const hasValidExport =
    hasDefaultExport ||
    (isMiddleware && hasMiddlewareExport) ||
    (isProxy && hasProxyExport)

  const relativePath = relative(process.cwd(), pageFilePath)
  const resolvedPath = relativePath.startsWith('.')
    ? relativePath
    : `./${relativePath}`

  if (!hasValidExport) {
    const message =
      `The file "${resolvedPath}" must export a function, either as a default export or as a named "${fileName}" export.\n` +
      `This function is what Next.js runs for every request handled by this ${fileName === 'proxy' ? 'proxy (previously called middleware)' : 'middleware'}.\n\n` +
      `Why this happens:\n` +
      (isProxy
        ? "- You are migrating from `middleware` to `proxy`, but haven't updated the exported function.\n"
        : '') +
      `- The file exists but doesn't export a function.\n` +
      `- The export is not a function (e.g., an object or constant).\n` +
      `- There's a syntax error preventing the export from being recognized.\n\n` +
      `To fix it:\n` +
      `- Ensure this file has either a default or "${fileName}" function export.\n\n` +
      `Learn more: https://nextjs.org/docs/messages/middleware-to-proxy`

    if (isDev) {
      // errorOnce as proxy/middleware runs per request including multiple
      // internal _next/ routes and spams logs.
      Log.errorOnce(message)
    } else {
      throw new Error(message)
    }
  }
}

function tryToReadFile(filePath: string, shouldThrow: boolean) {
  try {
    return readFileSync(filePath, {
      encoding: 'utf8',
    })
  } catch (error: any) {
    if (shouldThrow) {
      error.message = `Next.js ERROR: Failed to read file ${filePath}:\n${error.message}`
      throw error
    }
  }
}

/**
 * @internal - required to exclude zod types from the build
 */
export function getMiddlewareMatchers(
  matcherOrMatchers: MiddlewareConfigMatcherInput,
  nextConfig: Pick<NextConfig, 'basePath' | 'i18n'>
): ProxyMatcher[] {
  const matchers = Array.isArray(matcherOrMatchers)
    ? matcherOrMatchers
    : [matcherOrMatchers]

  const { i18n } = nextConfig

  return matchers.map((matcher) => {
    matcher = typeof matcher === 'string' ? { source: matcher } : matcher

    const originalSource = matcher.source

    let { source, ...rest } = matcher

    const isRoot = source === '/'

    if (i18n?.locales && matcher.locale !== false) {
      source = `/:nextInternalLocale((?!_next/)[^/.]{1,})${
        isRoot ? '' : source
      }`
    }

    source = `/:nextData(_next/data/[^/]{1,})?${source}${
      isRoot
        ? `(${nextConfig.i18n ? '|\\.json|' : ''}/?index|/?index\\.json)?`
        : '{(\\.json)}?'
    }`

    if (nextConfig.basePath) {
      source = `${nextConfig.basePath}${source}`
    }

    // Validate that the source is still.
    const result = SourceSchema.safeParse(source)
    if (!result.success) {
      reportZodError('Failed to parse middleware source', result.error)

      // We need to exit here because middleware being built occurs before we
      // finish setting up the server. Exiting here is the only way to ensure
      // that we don't hang.
      process.exit(1)
    }

    return {
      ...rest,
      // We know that parsed.regexStr is not undefined because we already
      // checked that the source is valid.
      regexp: tryToParsePath(result.data).regexStr!,
      originalSource: originalSource || source,
    }
  })
}

function parseMiddlewareConfig(
  page: string,
  rawConfig: unknown,
  nextConfig: NextConfig
): ProxyConfig {
  // If there's no config to parse, then return nothing.
  if (typeof rawConfig !== 'object' || !rawConfig) return {}

  const input = MiddlewareConfigInputSchema.safeParse(rawConfig)
  if (!input.success) {
    reportZodError(`${page} contains invalid middleware config`, input.error)

    // We need to exit here because middleware being built occurs before we
    // finish setting up the server. Exiting here is the only way to ensure
    // that we don't hang.
    process.exit(1)
  }

  const config: ProxyConfig = {}

  if (input.data.matcher) {
    config.matchers = getMiddlewareMatchers(input.data.matcher, nextConfig)
  }

  if (input.data.unstable_allowDynamic) {
    config.unstable_allowDynamic = Array.isArray(
      input.data.unstable_allowDynamic
    )
      ? input.data.unstable_allowDynamic
      : [input.data.unstable_allowDynamic]
  }

  if (input.data.regions) {
    config.regions = input.data.regions
  }

  return config
}

const apiRouteWarnings = new LRUCache(250)
function warnAboutExperimentalEdge(apiRoute: string | null) {
  if (
    process.env.NODE_ENV === 'production' &&
    process.env.NEXT_PRIVATE_BUILD_WORKER === '1'
  ) {
    return
  }

  if (apiRoute && apiRouteWarnings.has(apiRoute)) {
    return
  }

  Log.warn(
    apiRoute
      ? `${apiRoute} provided runtime 'experimental-edge'. It can be updated to 'edge' instead.`
      : `You are using an experimental edge runtime, the API might change.`
  )

  if (apiRoute) {
    apiRouteWarnings.set(apiRoute, 1)
  }
}

let hadUnsupportedValue = false
const warnedUnsupportedValueMap = new LRUCache<boolean>(250, () => 1)

function warnAboutUnsupportedValue(
  pageFilePath: string,
  page: string | undefined,
  result: { unsupported: string; path?: string }
) {
  hadUnsupportedValue = true
  const isProductionBuild = process.env.NODE_ENV === 'production'
  if (
    // we only log for the server compilation so it's not
    // duplicated due to webpack build worker having fresh
    // module scope for each compiler
    process.env.NEXT_COMPILER_NAME !== 'server' ||
    (isProductionBuild && warnedUnsupportedValueMap.has(pageFilePath))
  ) {
    return
  }
  warnedUnsupportedValueMap.set(pageFilePath, true)

  const message =
    `Next.js can't recognize the exported \`config\` field in ` +
    (page ? `route "${page}"` : `"${pageFilePath}"`) +
    ':\n' +
    result.unsupported +
    (result.path ? ` at "${result.path}"` : '') +
    '.\n' +
    'Read More - https://nextjs.org/docs/messages/invalid-page-config'

  // for a build we use `Log.error` instead of throwing
  // so that all errors can be logged before exiting the process
  if (isProductionBuild) {
    Log.error(message)
  } else {
    throw new Error(message)
  }
}

type GetPageStaticInfoParams = {
  pageFilePath: string
  nextConfig: Partial<NextConfig>
  isDev: boolean
  page: string
  pageType: PAGE_TYPES
}

export async function getAppPageStaticInfo({
  pageFilePath,
  nextConfig,
  isDev,
  page,
}: GetPageStaticInfoParams): Promise<AppPageStaticInfo> {
  const content = tryToReadFile(pageFilePath, !isDev)
  if (!content || !PARSE_PATTERN.test(content)) {
    return {
      type: PAGE_TYPES.APP,
      config: undefined,
      runtime: undefined,
      preferredRegion: undefined,
      maxDuration: undefined,
      hadUnsupportedValue: false,
    }
  }

  const ast = await parseModule(pageFilePath, content)
  validateMiddlewareProxyExports({
    ast,
    page,
    pageFilePath,
    isDev,
  })

  const {
    generateStaticParams,
    generateImageMetadata,
    generateSitemaps,
    exports,
    directives,
  } = checkExports(ast, AppSegmentConfigSchemaKeys, page)

  const { type: rsc } = getRSCModuleInformation(content, true)

  const exportedConfig: Record<string, unknown> = {}
  if (exports) {
    for (const property of exports) {
      const result = extractExportedConstValue(ast, property)
      if (result !== null && 'unsupported' in result) {
        warnAboutUnsupportedValue(pageFilePath, page, result)
      } else if (result !== null) {
        exportedConfig[property] = result.value
      }
    }
  }

  const configResult = extractExportedConstValue(ast, 'config')
  if (configResult !== null && 'unsupported' in configResult) {
    warnAboutUnsupportedValue(pageFilePath, page, configResult)
  } else if (configResult !== null) {
    exportedConfig.config = configResult.value
  }

  const route = normalizeAppPath(page)
  const config = parseAppSegmentConfig(exportedConfig, route)

  // Prevent edge runtime and generateStaticParams in the same file.
  if (isEdgeRuntime(config.runtime) && generateStaticParams) {
    throw new Error(
      `Page "${page}" cannot use both \`export const runtime = 'edge'\` and export \`generateStaticParams\`.`
    )
  }

  // Prevent use client and generateStaticParams in the same file.
  if (directives?.has('client') && generateStaticParams) {
    throw new Error(
      `Page "${page}" cannot use both "use client" and export function "generateStaticParams()".`
    )
  }

  // Prevent use client and unstable_instant in the same file.
  if (directives?.has('client') && 'unstable_instant' in config) {
    throw new Error(
      `"unstable_instant" is a route segment config and can only be used when the segment is a Server Component module. Remove the "use client" directive from "${pageFilePath}" to use this API.`
    )
  }

  if ('unstable_instant' in config && !nextConfig.cacheComponents) {
    throw new Error(
      `Route "${page}" cannot use \`export const unstable_instant = ...\` without enabling \`cacheComponents\`.`
    )
  }

  // Prevent use client and unstable_prefetch in the same file.
  if (directives?.has('client') && 'unstable_prefetch' in config) {
    throw new Error(
      `"unstable_prefetch" is a route segment config and can only be used when the segment is a Server Component module. Remove the "use client" directive from "${pageFilePath}" to use this API.`
    )
  }

  if ('unstable_prefetch' in config && !nextConfig.cacheComponents) {
    throw new Error(
      `Route "${page}" cannot use \`export const unstable_prefetch = ...\` without enabling \`cacheComponents\`.`
    )
  }

  // Prevent unstable_dynamicStaleTime in layouts.
  if ('unstable_dynamicStaleTime' in config) {
    const isLayout = /\/layout\.[^/]+$/.test(pageFilePath)
    if (isLayout) {
      throw new Error(
        `"${page}" cannot use \`export const unstable_dynamicStaleTime\`. This config is only supported in page files, not layouts.`
      )
    }
  }

  // Prevent combining unstable_dynamicStaleTime and unstable_instant.
  if ('unstable_dynamicStaleTime' in config && 'unstable_instant' in config) {
    throw new Error(
      `Page "${page}" cannot use both \`export const unstable_dynamicStaleTime\` and \`export const unstable_instant\`.`
    )
  }

  if (isEdgeRuntime(config.runtime)) {
    warnAboutEdgeRuntime()
  }

  if (config.preferredRegion !== undefined) {
    warnAboutPreferredRegion()
  }

  return {
    type: PAGE_TYPES.APP,
    rsc,
    generateImageMetadata,
    generateSitemaps,
    generateStaticParams,
    config,
    middleware: parseMiddlewareConfig(page, exportedConfig.config, nextConfig),
    runtime: config.runtime,
    preferredRegion: config.preferredRegion,
    maxDuration: config.maxDuration,
    hadUnsupportedValue,
  }
}

export async function getPagesPageStaticInfo({
  pageFilePath,
  nextConfig,
  isDev,
  page,
}: GetPageStaticInfoParams): Promise<PagesPageStaticInfo> {
  const content = tryToReadFile(pageFilePath, !isDev)
  if (!content || !PARSE_PATTERN.test(content)) {
    return {
      type: PAGE_TYPES.PAGES,
      config: undefined,
      runtime: undefined,
      preferredRegion: undefined,
      maxDuration: undefined,
      hadUnsupportedValue: false,
    }
  }

  const ast = await parseModule(pageFilePath, content)
  validateMiddlewareProxyExports({
    ast,
    page,
    pageFilePath,
    isDev,
  })

  const { getServerSideProps, getStaticProps, exports } = checkExports(
    ast,
    PagesSegmentConfigSchemaKeys,
    page
  )

  const { type: rsc } = getRSCModuleInformation(content, true)

  const exportedConfig: Record<string, unknown> = {}
  if (exports) {
    for (const property of exports) {
      const result = extractExportedConstValue(ast, property)
      if (result !== null && 'unsupported' in result) {
        warnAboutUnsupportedValue(pageFilePath, page, result)
      } else if (result !== null) {
        exportedConfig[property] = result.value
      }
    }
  }

  const configResult = extractExportedConstValue(ast, 'config')
  if (configResult !== null && 'unsupported' in configResult) {
    warnAboutUnsupportedValue(pageFilePath, page, configResult)
  } else if (configResult !== null) {
    exportedConfig.config = configResult.value
  }

  // Validate the config.
  const route = normalizePagePath(page)
  const config = parsePagesSegmentConfig(exportedConfig, route)
  const isAnAPIRoute = isAPIRoute(route)

  let resolvedRuntime = config.runtime ?? config.config?.runtime

  if (isProxyFile(page) && resolvedRuntime) {
    const relativePath = relative(process.cwd(), pageFilePath)
    const resolvedPath = relativePath.startsWith('.')
      ? relativePath
      : `./${relativePath}`
    const message = `Route segment config is not allowed in Proxy file at "${resolvedPath}". Proxy always runs on Node.js runtime. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy`

    if (isDev) {
      // errorOnce as proxy/middleware runs per request including multiple
      // internal _next/ routes and spams logs.
      Log.errorOnce(message)
      resolvedRuntime = SERVER_RUNTIME.nodejs
    } else {
      throw new Error(message)
    }
  }

  if (resolvedRuntime === SERVER_RUNTIME.experimentalEdge) {
    warnAboutExperimentalEdge(isAnAPIRoute ? page! : null)
  }

  if (
    !isProxyFile(page) &&
    resolvedRuntime === SERVER_RUNTIME.edge &&
    page &&
    !isAnAPIRoute
  ) {
    const message = `Page ${page} provided runtime 'edge', the edge runtime for rendering is currently experimental. Use runtime 'experimental-edge' instead.`
    if (isDev) {
      Log.error(message)
    } else {
      throw new Error(message)
    }
  }

  if (isEdgeRuntime(resolvedRuntime)) {
    warnAboutEdgeRuntime()
  }

  if (config.config?.regions !== undefined) {
    warnAboutPreferredRegion()
  }

  return {
    type: PAGE_TYPES.PAGES,
    getStaticProps,
    getServerSideProps,
    rsc,
    config,
    middleware: parseMiddlewareConfig(page, exportedConfig.config, nextConfig),
    runtime: resolvedRuntime,
    preferredRegion: config.config?.regions,
    maxDuration: config.maxDuration ?? config.config?.maxDuration,
    hadUnsupportedValue,
  }
}

/**
 * For a given pageFilePath and nextConfig, if the config supports it, this
 * function will read the file and return the runtime that should be used.
 * It will look into the file content only if the page *requires* a runtime
 * to be specified, that is, when gSSP or gSP is used.
 * Related discussion: https://github.com/vercel/next.js/discussions/34179
 */
export async function getPageStaticInfo(
  params: GetPageStaticInfoParams
): Promise<PageStaticInfo> {
  if (params.pageType === PAGE_TYPES.APP) {
    return getAppPageStaticInfo(params)
  }

  return getPagesPageStaticInfo(params)
}
Quest for Codev2.0.0
/
SIGN IN