next.js/packages/next/src/build/utils.ts
utils.ts1658 lines47.4 KB
import type {
  NextConfigComplete,
  NextConfigRuntime,
} from '../server/config-shared'
import type { ExperimentalPPRConfig } from '../server/lib/experimental/ppr'
import { checkIsRoutePPREnabled } from '../server/lib/experimental/ppr'
import type { AssetBinding } from './webpack/loaders/get-module-build-info'
import type { ServerRuntime } from '../types'
import type { BuildManifest } from '../server/get-page-files'
import {
  normalizeRouteRegex,
  type CustomRoutes,
  type Header,
  type Redirect,
  type Rewrite,
} from '../lib/load-custom-routes'
import type {
  EdgeFunctionDefinition,
  MiddlewareManifest,
} from './webpack/plugins/middleware-plugin'
import type { WebpackLayerName } from '../lib/constants'
import {
  INSTRUMENTATION_HOOK_FILENAME,
  MIDDLEWARE_FILENAME,
  SERVER_PROPS_GET_INIT_PROPS_CONFLICT,
  SERVER_PROPS_SSG_CONFLICT,
  SSG_GET_INITIAL_PROPS_CONFLICT,
  WEBPACK_LAYERS,
  PROXY_FILENAME,
} from '../lib/constants'
import type {
  AppPageModule,
  AppPageRouteModule,
} from '../server/route-modules/app-page/module'
import type { NextComponentType } from '../shared/lib/utils'

import '../server/require-hook'
import '../server/node-polyfill-crypto'
import '../server/node-environment'

import { bold, cyan, green, red, underline, yellow } from '../lib/picocolors'
import textTable from 'next/dist/compiled/text-table'
import path from 'path'
import { promises as fs } from 'fs'
import { isValidElementType } from 'next/dist/compiled/react-is'
import stripAnsi from 'next/dist/compiled/strip-ansi'
import {
  UNDERSCORE_GLOBAL_ERROR_ROUTE,
  UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY,
  UNDERSCORE_NOT_FOUND_ROUTE,
} from '../shared/lib/constants'
import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic'
import { findPageFile } from '../server/lib/find-page-file'
import { isEdgeRuntime } from '../lib/is-edge-runtime'
import * as Log from './output/log'
import type { LoadComponentsReturnType } from '../server/load-components'
import { loadComponents } from '../server/load-components'
import { trace } from '../trace'
import { setHttpClientAndAgentOptions } from '../server/setup-http-agent-env'
import { Sema } from 'next/dist/compiled/async-sema'
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import { getRuntimeContext } from '../server/web/sandbox'
import { RouteKind } from '../server/route-kind'
import type { PageExtensions } from './page-extensions-type'
import type { FallbackMode } from '../lib/fallback'
import type { OutgoingHttpHeaders } from 'http'
import type { AppSegmentConfig } from './segment-config/app/app-segment-config'
import type { AppSegment } from './segment-config/app/app-segments'
import { collectSegments } from './segment-config/app/app-segments'
import { createIncrementalCache } from '../export/helpers/create-incremental-cache'
import { collectRootParamKeys } from './segment-config/app/collect-root-param-keys'
import { buildAppStaticPaths } from './static-paths/app'
import { buildPagesStaticPaths } from './static-paths/pages'
import type { PrerenderedRoute } from './static-paths/types'
import type { CacheControl } from '../server/lib/cache-control'
import { formatExpire, formatRevalidate } from './output/format'
import type {
  AppRouteModule,
  AppRouteRouteModule,
} from '../server/route-modules/app-route/module'
import type { FunctionsConfigManifest, ManifestRoute } from './index'
import { getNamedRouteRegex } from '../shared/lib/router/utils/route-regex'
import { parseNormalizedAppRoute } from '../shared/lib/router/routes/app'
import { fillStaticMetadataSegment } from '../lib/metadata/get-metadata-route'
import { STATIC_METADATA_IMAGES } from '../lib/metadata/is-metadata-route'

// Build a set of static metadata image filenames for quick lookup
const staticMetadataImageFilenames = new Set<string>(
  Object.values(STATIC_METADATA_IMAGES).map((meta) => meta.filename)
)

/**
 * Get the display path for build output. For static metadata files under
 * dynamic routes, this normalizes the path to use "-" placeholder.
 * e.g., /dynamic/[id]/icon.png -> /dynamic/-/icon.png
 */
function getTreeViewDisplayPath(pagePath: string): string {
  // Check if the path contains dynamic segments
  if (!isDynamicRoute(pagePath)) {
    return pagePath
  }

  // Check if the filename is a static metadata image
  const lastSlash = pagePath.lastIndexOf('/')
  const filename = pagePath.slice(lastSlash + 1)
  const dotIndex = filename.lastIndexOf('.')
  const baseName = dotIndex > 0 ? filename.slice(0, dotIndex) : filename

  // Check against known static metadata image filenames (e.g., icon, apple-icon, opengraph-image)
  if (!staticMetadataImageFilenames.has(baseName)) {
    return pagePath
  }

  // Transform using the static metadata resolver so dynamic segments use "-"
  const segment = pagePath.slice(0, lastSlash)
  const lastSegment = filename
  return fillStaticMetadataSegment(segment, lastSegment)
}

export type ROUTER_TYPE = 'pages' | 'app'

export type DynamicManifestRoute = ManifestRoute & {
  /**
   * The source page that this route is based on. This is used to determine the
   * source page for the route and is only relevant for app pages where PPR is
   * enabled and the page differs from the source page.
   */
  sourcePage: string | undefined
}

// Use `print()` for expected console output
const print = console.log

const RESERVED_PAGE = /^\/(_app|_error|_document|api(\/|$))/

export function unique<T>(main: ReadonlyArray<T>, sub: ReadonlyArray<T>): T[] {
  return [...new Set([...main, ...sub])]
}

export function difference<T>(
  main: ReadonlyArray<T> | ReadonlySet<T>,
  sub: ReadonlyArray<T> | ReadonlySet<T>
): T[] {
  const a = new Set(main)
  const b = new Set(sub)
  return [...a].filter((x) => !b.has(x))
}

export function isMiddlewareFilename(file?: string | null) {
  return (
    file === MIDDLEWARE_FILENAME ||
    file === `src/${MIDDLEWARE_FILENAME}` ||
    file === PROXY_FILENAME ||
    file === `src/${PROXY_FILENAME}`
  )
}

export function isInstrumentationHookFilename(file?: string | null) {
  return (
    file === INSTRUMENTATION_HOOK_FILENAME ||
    file === `src/${INSTRUMENTATION_HOOK_FILENAME}`
  )
}

export const filterAndSortList = (
  list: ReadonlyArray<string>,
  routeType: ROUTER_TYPE,
  hasCustomApp: boolean
) => {
  let pages: string[]
  if (routeType === 'app') {
    // filter out static app route of /favicon.ico and /_global-error
    pages = list.filter((e) => {
      if (e === '/favicon.ico') return false
      // Hide static /_global-error from build output
      if (e === '/_global-error') return false
      return true
    })
  } else {
    // filter built-in pages
    pages = list
      .slice()
      .filter(
        (e) =>
          !(
            e === '/_document' ||
            e === '/_error' ||
            (!hasCustomApp && e === '/_app')
          )
      )
  }
  return pages.sort((a, b) => a.localeCompare(b))
}

export interface PageInfo {
  originalAppPath: string | undefined
  isStatic: boolean
  isSSG: boolean
  /**
   * If true, it means that the route has partial prerendering enabled.
   */
  isRoutePPREnabled: boolean
  ssgPageRoutes: string[] | null
  initialCacheControl: CacheControl | undefined
  pageDuration: number | undefined
  ssgPageDurations: number[] | undefined
  runtime: ServerRuntime
  hasEmptyStaticShell?: boolean
  hasPostponed?: boolean
  isDynamicAppRoute?: boolean
}

export type PageInfos = Map<string, PageInfo>

function getTreeViewSymbol(
  item: string,
  pageInfo: PageInfo | undefined
): string {
  if (item === '/_app' || item === '/_app.server') {
    return ' '
  }

  if (isEdgeRuntime(pageInfo?.runtime)) {
    return 'ƒ'
  }

  if (pageInfo?.isRoutePPREnabled) {
    if (
      // If the page has an empty static shell, then it's equivalent to a
      // dynamic page
      pageInfo?.hasEmptyStaticShell ||
      // ensure we don't mark dynamic paths that postponed as being dynamic
      // since in this case we're able to partially prerender it
      (pageInfo.isDynamicAppRoute && !pageInfo.hasPostponed)
    ) {
      return 'ƒ'
    }

    if (!pageInfo?.hasPostponed) {
      return '○'
    }

    return '◐'
  }

  if (pageInfo?.isStatic) {
    return '○'
  }

  if (pageInfo?.isSSG) {
    return '●'
  }

  return 'ƒ'
}

export interface RoutesUsingEdgeRuntime {
  [route: string]: 0
}

export function collectRoutesUsingEdgeRuntime(
  input: PageInfos
): RoutesUsingEdgeRuntime {
  const routesUsingEdgeRuntime: RoutesUsingEdgeRuntime = {}
  for (const [route, info] of input.entries()) {
    if (isEdgeRuntime(info.runtime)) {
      routesUsingEdgeRuntime[route] = 0
    }
  }

  return routesUsingEdgeRuntime
}

export async function printTreeView(
  lists: {
    pages: ReadonlyArray<string>
    app: ReadonlyArray<string> | undefined
  },
  pageInfos: Map<string, PageInfo>,
  {
    pagesDir,
    pageExtensions,
    middlewareManifest,
    functionsConfigManifest,
    useStaticPages404,
    hasGSPAndRevalidateZero,
  }: {
    pagesDir?: string
    pageExtensions: PageExtensions
    buildManifest: BuildManifest
    middlewareManifest: MiddlewareManifest
    functionsConfigManifest: FunctionsConfigManifest
    useStaticPages404: boolean
    hasGSPAndRevalidateZero: Set<string>
  }
) {
  // Can be overridden for test purposes to omit the build duration output.
  const MIN_DURATION = process.env.__NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT
    ? Infinity // Don't ever log build durations.
    : 300

  const getPrettyDuration = (_duration: number): string => {
    const duration = `${_duration} ms`
    // green for 300-1000ms
    if (_duration < 1000) return green(duration)
    // yellow for 1000-2000ms
    if (_duration < 2000) return yellow(duration)
    // red for >= 2000ms
    return red(bold(duration))
  }

  // Check if we have a custom app.
  const hasCustomApp = !!(
    pagesDir && (await findPageFile(pagesDir, '/_app', pageExtensions, false))
  )

  // Collect all the symbols we use so we can print the icons out.
  const usedSymbols = new Set()

  const messages: string[][] = []

  const printFileTree = async ({
    list,
    routerType,
  }: {
    list: ReadonlyArray<string>
    routerType: ROUTER_TYPE
  }) => {
    const filteredPages = filterAndSortList(list, routerType, hasCustomApp)
    if (filteredPages.length === 0) {
      return
    }

    let showRevalidate = false
    let showExpire = false

    for (const page of filteredPages) {
      const cacheControl = pageInfos.get(page)?.initialCacheControl

      if (cacheControl?.revalidate) {
        showRevalidate = true
      }

      if (cacheControl?.expire) {
        showExpire = true
      }

      if (showRevalidate && showExpire) {
        break
      }
    }

    messages.push(
      [
        routerType === 'app' ? 'Route (app)' : 'Route (pages)',
        showRevalidate ? 'Revalidate' : '',
        showExpire ? 'Expire' : '',
      ]
        .filter((entry) => entry !== '')
        .map((entry) => underline(entry))
    )

    filteredPages.forEach((item, i, arr) => {
      const border =
        i === 0
          ? arr.length === 1
            ? '─'
            : '┌'
          : i === arr.length - 1
            ? '└'
            : '├'

      const pageInfo = pageInfos.get(item)
      const totalDuration =
        (pageInfo?.pageDuration || 0) +
        (pageInfo?.ssgPageDurations?.reduce((a, b) => a + (b || 0), 0) || 0)

      const symbol = getTreeViewSymbol(item, pageInfo)
      const hasChildRoutes = Boolean(pageInfo?.ssgPageRoutes?.length)

      const displayPath = getTreeViewDisplayPath(item)

      if (hasGSPAndRevalidateZero.has(item)) {
        usedSymbols.add('ƒ')
        messages.push([
          `${border} ƒ ${displayPath}${
            totalDuration > MIN_DURATION
              ? ` (${getPrettyDuration(totalDuration)})`
              : ''
          }`,
          showRevalidate && pageInfo?.initialCacheControl
            ? formatRevalidate(pageInfo.initialCacheControl)
            : '',
          showExpire && pageInfo?.initialCacheControl
            ? formatExpire(pageInfo.initialCacheControl)
            : '',
        ])
      }

      // Grouped rows act as headers for the generated outputs below them. The
      // child rows carry the concrete route symbols instead.
      if (!hasChildRoutes) {
        usedSymbols.add(symbol)
      }

      messages.push([
        `${border} ${hasChildRoutes ? ' ' : symbol} ${displayPath}${
          totalDuration > MIN_DURATION
            ? ` (${getPrettyDuration(totalDuration)})`
            : ''
        }`,
        showRevalidate && pageInfo?.initialCacheControl
          ? formatRevalidate(pageInfo.initialCacheControl)
          : '',
        showExpire && pageInfo?.initialCacheControl
          ? formatExpire(pageInfo.initialCacheControl)
          : '',
      ])

      if (pageInfo?.ssgPageRoutes?.length) {
        const totalRoutes = pageInfo.ssgPageRoutes.length
        const contSymbol = i === arr.length - 1 ? ' ' : '│'

        // HERE

        let routes: { route: string; duration: number; avgDuration?: number }[]
        if (pageInfo.ssgPageDurations?.some((d) => d > MIN_DURATION)) {
          const previewPages = totalRoutes === 8 ? 8 : Math.min(totalRoutes, 7)
          const routesWithDuration = pageInfo.ssgPageRoutes
            .map((route, idx) => ({
              route,
              duration: pageInfo.ssgPageDurations![idx] || 0,
            }))
            .sort(({ duration: a }, { duration: b }) =>
              // Sort by duration
              // keep too small durations in original order at the end
              a <= MIN_DURATION && b <= MIN_DURATION ? 0 : b - a
            )
          routes = routesWithDuration.slice(0, previewPages)
          const remainingRoutes = routesWithDuration.slice(previewPages)
          if (remainingRoutes.length) {
            const remaining = remainingRoutes.length
            const avgDuration = Math.round(
              remainingRoutes.reduce(
                (total, { duration }) => total + duration,
                0
              ) / remainingRoutes.length
            )
            routes.push({
              route: `[+${remaining} more paths]`,
              duration: 0,
              avgDuration,
            })
          }
        } else {
          const previewPages = totalRoutes === 4 ? 4 : Math.min(totalRoutes, 3)
          routes = pageInfo.ssgPageRoutes
            .slice(0, previewPages)
            .map((route) => ({ route, duration: 0 }))
          if (totalRoutes > previewPages) {
            const remaining = totalRoutes - previewPages
            routes.push({ route: `[+${remaining} more paths]`, duration: 0 })
          }
        }

        routes.forEach(
          ({ route, duration, avgDuration }, index, { length }) => {
            const innerSymbol = index === length - 1 ? '└' : '├'
            // Generated child paths can have more precise metadata than the
            // parent route pattern, so prefer the child entry when present.
            const routePageInfo = pageInfos.get(route) ?? pageInfo
            const routeSymbol = getTreeViewSymbol(route, routePageInfo)
            usedSymbols.add(routeSymbol)

            const initialCacheControl =
              pageInfos.get(route)?.initialCacheControl

            messages.push([
              `${contSymbol} ${innerSymbol} ${routeSymbol} ${route}${
                duration > MIN_DURATION
                  ? ` (${getPrettyDuration(duration)})`
                  : ''
              }${
                avgDuration && avgDuration > MIN_DURATION
                  ? ` (avg ${getPrettyDuration(avgDuration)})`
                  : ''
              }`,
              showRevalidate && initialCacheControl
                ? formatRevalidate(initialCacheControl)
                : '',
              showExpire && initialCacheControl
                ? formatExpire(initialCacheControl)
                : '',
            ])
          }
        )
      }
    })
  }

  // If enabled, then print the tree for the app directory.
  if (lists.app) {
    await printFileTree({
      routerType: 'app',
      list: lists.app,
    })

    messages.push(['', '', '', ''])
  }

  pageInfos.set('/404', {
    ...(pageInfos.get('/404') || pageInfos.get('/_error'))!,
    isStatic: useStaticPages404,
  })

  // If there's no app /_notFound page present, then the 404 is still using the pages/404
  if (
    !lists.pages.includes('/404') &&
    !lists.app?.includes(UNDERSCORE_NOT_FOUND_ROUTE)
  ) {
    lists.pages = [...lists.pages, '/404']
  }

  // Print the tree view for the pages directory.
  await printFileTree({
    routerType: 'pages',
    list: lists.pages,
  })

  if (
    middlewareManifest.middleware?.['/']?.files.length > 0 ||
    // 'nodejs' runtime middleware or proxy is set to
    // functions-config-manifest instead of middleware-manifest.
    functionsConfigManifest.functions?.['/_middleware']
  ) {
    messages.push([])
    messages.push(['ƒ Proxy (Middleware)'])
  }

  print(
    textTable(messages, {
      align: ['l', 'r', 'r', 'r'],
      stringLength: (str) => stripAnsi(str).length,
    })
  )

  const staticFunctionInfo = lists.app
    ? 'generateStaticParams'
    : 'getStaticProps'
  print()
  print(
    textTable(
      [
        usedSymbols.has('○') && [
          '○',
          '(Static)',
          'prerendered as static content',
        ],
        usedSymbols.has('●') && [
          '●',
          '(SSG)',
          `prerendered as static HTML (uses ${cyan(staticFunctionInfo)})`,
        ],
        usedSymbols.has('◐') && [
          '◐',
          '(Partial Prerender)',
          'prerendered as static HTML with dynamic server-streamed content',
        ],
        usedSymbols.has('ƒ') && ['ƒ', '(Dynamic)', `server-rendered on demand`],
      ].filter((x) => x) as [string, string, string][],
      {
        align: ['l', 'l', 'l'],
        stringLength: (str) => stripAnsi(str).length,
      }
    )
  )

  print()
}

export function printCustomRoutes({
  redirects,
  rewrites,
  headers,
  onMatchHeaders,
}: CustomRoutes) {
  const printRoutes = (
    routes: Redirect[] | Rewrite[] | Header[],
    type: 'Redirects' | 'Rewrites' | 'Headers' | 'On Match Headers'
  ) => {
    const isRedirects = type === 'Redirects'
    const isHeaders = type === 'Headers' || type === 'On Match Headers'
    print(underline(type))

    /*
        ┌ source
        ├ permanent/statusCode
        └ destination
     */
    const routesStr = (routes as any[])
      .map((route: { source: string }) => {
        let routeStr = `┌ source: ${route.source}\n`

        if (!isHeaders) {
          const r = route as Rewrite
          routeStr += `${isRedirects ? '├' : '└'} destination: ${
            r.destination
          }\n`
        }
        if (isRedirects) {
          const r = route as Redirect
          routeStr += `└ ${
            r.statusCode
              ? `status: ${r.statusCode}`
              : `permanent: ${r.permanent}`
          }\n`
        }

        if (isHeaders) {
          const r = route as Header
          routeStr += `└ headers:\n`

          for (let i = 0; i < r.headers.length; i++) {
            const header = r.headers[i]
            const last = i === headers.length - 1

            routeStr += `  ${last ? '└' : '├'} ${header.key}: ${header.value}\n`
          }
        }

        return routeStr
      })
      .join('\n')

    print(`${routesStr}\n`)
  }

  print()
  if (redirects.length) {
    printRoutes(redirects, 'Redirects')
  }
  if (headers.length) {
    printRoutes(headers, 'Headers')
  }
  if (onMatchHeaders.length) {
    printRoutes(onMatchHeaders, 'On Match Headers')
  }

  const combinedRewrites = [
    ...rewrites.beforeFiles,
    ...rewrites.afterFiles,
    ...rewrites.fallback,
  ]
  if (combinedRewrites.length) {
    printRoutes(combinedRewrites, 'Rewrites')
  }
}

type PageIsStaticResult = {
  isRoutePPREnabled?: boolean
  isStatic?: boolean
  hasServerProps?: boolean
  hasStaticProps?: boolean
  prerenderedRoutes: PrerenderedRoute[] | undefined
  prerenderFallbackMode: FallbackMode | undefined
  rootParamKeys: readonly string[] | undefined
  isNextImageImported?: boolean
  traceIncludes?: string[]
  traceExcludes?: string[]
  appConfig?: AppSegmentConfig
}

export async function isPageStatic({
  dir,
  page,
  distDir,
  configFileName,
  httpAgentOptions,
  locales,
  defaultLocale,
  parentId,
  pageRuntime,
  edgeInfo,
  pageType,
  cacheComponents,
  authInterrupts,
  useCacheTimeout,
  staticPageGenerationTimeout,
  originalAppPath,
  isrFlushToDisk,
  cacheMaxMemorySize,
  nextConfigOutput,
  cacheHandler,
  cacheHandlers,
  cacheLifeProfiles,
  pprConfig,
  partialFallbacksEnabled,
  buildId,
  deploymentId,
  clientAssetToken,
  sriEnabled,
}: {
  dir: string
  page: string
  distDir: string
  cacheComponents: boolean
  authInterrupts: boolean
  useCacheTimeout: number
  staticPageGenerationTimeout: number
  configFileName: string
  httpAgentOptions: NextConfigComplete['httpAgentOptions']
  locales?: readonly string[]
  defaultLocale?: string
  parentId?: any
  edgeInfo?: any
  pageType?: 'pages' | 'app'
  pageRuntime?: ServerRuntime
  originalAppPath?: string
  isrFlushToDisk?: boolean
  cacheMaxMemorySize: number
  cacheHandler?: string
  cacheHandlers?: Record<string, string | undefined>
  cacheLifeProfiles?: {
    [profile: string]: import('../server/use-cache/cache-life').CacheLife
  }
  nextConfigOutput: 'standalone' | 'export' | undefined
  pprConfig: ExperimentalPPRConfig | undefined
  partialFallbacksEnabled: boolean
  buildId: string
  deploymentId: string
  clientAssetToken: string
  sriEnabled: boolean
}): Promise<PageIsStaticResult> {
  // Skip page data collection for synthetic _global-error routes
  if (page === UNDERSCORE_GLOBAL_ERROR_ROUTE) {
    return {
      isStatic: true,
      isRoutePPREnabled: false,
      prerenderFallbackMode: undefined,
      prerenderedRoutes: undefined,
      rootParamKeys: undefined,
      hasStaticProps: false,
      hasServerProps: false,
      isNextImageImported: false,
      appConfig: {},
    }
  }

  await createIncrementalCache({
    cacheHandler,
    cacheHandlers,
    distDir,
    dir,
    flushToDisk: isrFlushToDisk,
    cacheMaxMemorySize,
  })

  const isPageStaticSpan = trace('is-page-static-utils', parentId)
  return isPageStaticSpan
    .traceAsyncFn(async (): Promise<PageIsStaticResult> => {
      setHttpClientAndAgentOptions({
        httpAgentOptions,
      })

      let componentsResult: LoadComponentsReturnType
      let prerenderedRoutes: PrerenderedRoute[] | undefined
      let prerenderFallbackMode: FallbackMode | undefined
      let appConfig: AppSegmentConfig = {}
      let rootParamKeys: readonly string[] | undefined
      const pathIsEdgeRuntime = isEdgeRuntime(pageRuntime)

      if (pathIsEdgeRuntime) {
        const runtime = await getRuntimeContext({
          paths: edgeInfo.files.map((file: string) => path.join(distDir, file)),
          edgeFunctionEntry: {
            ...edgeInfo,
            wasm: (edgeInfo.wasm ?? []).map((binding: AssetBinding) => ({
              ...binding,
              filePath: path.join(distDir, binding.filePath),
            })),
          },
          name: edgeInfo.name,
          useCache: true,
          distDir,
          clientAssetToken,
        })
        const mod = (
          await runtime.context._ENTRIES[`middleware_${edgeInfo.name}`]
        ).ComponentMod

        // This is not needed during require.
        const buildManifest = {} as BuildManifest

        componentsResult = {
          Component: mod.default,
          Document: mod.Document,
          App: mod.App,
          routeModule: mod.routeModule,
          page,
          ComponentMod: mod,
          pageConfig: mod.config || {},
          buildManifest,
          reactLoadableManifest: {},
          getServerSideProps: mod.getServerSideProps,
          getStaticPaths: mod.getStaticPaths,
          getStaticProps: mod.getStaticProps,
        }
      } else {
        componentsResult = await loadComponents({
          distDir,
          page: originalAppPath || page,
          isAppPath: pageType === 'app',
          isDev: false,
          sriEnabled,
          needsManifestsForLegacyReasons: true,
        })
      }

      const { Component, routeModule } = componentsResult

      const Comp = Component as NextComponentType | undefined

      let isRoutePPREnabled: boolean = false

      if (pageType === 'app') {
        // @ts-expect-error pageType is app, so we can assume AppPageModule | AppRouteModule
        const ComponentMod: AppPageModule | AppRouteModule =
          componentsResult.ComponentMod

        let segments: AppSegment[]
        try {
          segments = await collectSegments(
            // We know this is an app page or app route module because we
            // checked above that the page type is 'app'.
            routeModule as AppPageRouteModule | AppRouteRouteModule
          )
        } catch (err) {
          throw new Error(`Failed to collect configuration for ${page}`, {
            cause: err,
          })
        }

        appConfig =
          originalAppPath === UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY
            ? {}
            : reduceAppConfig(segments)

        if (appConfig.dynamic === 'force-static' && pathIsEdgeRuntime) {
          Log.warn(
            `Page "${page}" is using runtime = 'edge' which is currently incompatible with dynamic = 'force-static'. Please remove either "runtime" or "force-static" for correct behavior`
          )
        }

        rootParamKeys = collectRootParamKeys(routeModule)

        // A page supports partial prerendering if it is an app page and either
        // the whole app has PPR enabled or this page has PPR enabled when we're
        // in incremental mode.
        isRoutePPREnabled =
          routeModule.definition.kind === RouteKind.APP_PAGE &&
          checkIsRoutePPREnabled(pprConfig)

        // If force dynamic was set and we don't have PPR enabled, then set the
        // revalidate to 0.
        // TODO: (PPR) remove this once PPR is enabled by default
        if (appConfig.dynamic === 'force-dynamic' && !isRoutePPREnabled) {
          appConfig.revalidate = 0
        }

        const route = parseNormalizedAppRoute(page)

        // If the page is dynamic and we're not in edge runtime, then we need to
        // build the static paths. The edge runtime doesn't support static
        // paths.
        if (route.dynamicSegments.length > 0 && !pathIsEdgeRuntime) {
          ;({ prerenderedRoutes, fallbackMode: prerenderFallbackMode } =
            await buildAppStaticPaths({
              dir,
              page,
              route,
              cacheComponents,
              authInterrupts,
              useCacheTimeout,
              staticPageGenerationTimeout,
              segments,
              distDir,
              requestHeaders: {},
              isrFlushToDisk,
              cacheMaxMemorySize,
              cacheHandler,
              cacheLifeProfiles,
              ComponentMod,
              nextConfigOutput,
              isRoutePPREnabled,
              partialFallbacksEnabled,
              buildId,
              deploymentId,
              rootParamKeys,
            }))
        }
      } else {
        if (!Comp || !isValidElementType(Comp) || typeof Comp === 'string') {
          throw new Error('INVALID_DEFAULT_EXPORT')
        }
      }

      const hasGetInitialProps = !!Comp?.getInitialProps
      const hasStaticProps = !!componentsResult.getStaticProps
      const hasStaticPaths = !!componentsResult.getStaticPaths
      const hasServerProps = !!componentsResult.getServerSideProps

      // A page cannot be prerendered _and_ define a data requirement. That's
      // contradictory!
      if (hasGetInitialProps && hasStaticProps) {
        throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT)
      }

      if (hasGetInitialProps && hasServerProps) {
        throw new Error(SERVER_PROPS_GET_INIT_PROPS_CONFLICT)
      }

      if (hasStaticProps && hasServerProps) {
        throw new Error(SERVER_PROPS_SSG_CONFLICT)
      }

      const pageIsDynamic = isDynamicRoute(page)
      // A page cannot have static parameters if it is not a dynamic page.
      if (hasStaticProps && hasStaticPaths && !pageIsDynamic) {
        throw new Error(
          `getStaticPaths can only be used with dynamic pages, not '${page}'.` +
            `\nLearn more: https://nextjs.org/docs/routing/dynamic-routes`
        )
      }

      if (hasStaticProps && pageIsDynamic && !hasStaticPaths) {
        throw new Error(
          `getStaticPaths is required for dynamic SSG pages and is missing for '${page}'.` +
            `\nRead more: https://nextjs.org/docs/messages/invalid-getstaticpaths-value`
        )
      }

      if (hasStaticProps && hasStaticPaths) {
        ;({ prerenderedRoutes, fallbackMode: prerenderFallbackMode } =
          await buildPagesStaticPaths({
            page,
            locales,
            defaultLocale,
            configFileName,
            getStaticPaths: componentsResult.getStaticPaths!,
          }))
      }

      const isNextImageImported = (globalThis as any).__NEXT_IMAGE_IMPORTED

      let isStatic = false
      if (!hasStaticProps && !hasGetInitialProps && !hasServerProps) {
        isStatic = true
      }

      // When PPR is enabled, any route may be completely static, so
      // mark this route as static.
      if (isRoutePPREnabled) {
        isStatic = true
      }

      return {
        isStatic,
        isRoutePPREnabled,
        prerenderFallbackMode,
        prerenderedRoutes,
        rootParamKeys,
        hasStaticProps,
        hasServerProps,
        isNextImageImported,
        appConfig,
      }
    })
    .catch((err) => {
      if (err.message === 'INVALID_DEFAULT_EXPORT') {
        throw err
      }
      console.error(err)
      throw new Error(`Failed to collect page data for ${page}`)
    })
}

type ReducedAppConfig = Pick<
  AppSegmentConfig,
  | 'revalidate'
  | 'dynamic'
  | 'fetchCache'
  | 'preferredRegion'
  | 'runtime'
  | 'maxDuration'
>

/**
 * Collect the app config from the generate param segments. This only gets a
 * subset of the config options.
 *
 * @param segments the generate param segments
 * @returns the reduced app config
 */
export function reduceAppConfig(
  segments: Pick<AppSegment, 'config'>[]
): ReducedAppConfig {
  const config: ReducedAppConfig = {}

  for (const segment of segments) {
    const {
      dynamic,
      fetchCache,
      preferredRegion,
      revalidate,
      runtime,
      maxDuration,
    } = segment.config || {}

    // TODO: should conflicting configs here throw an error
    // e.g. if layout defines one region but page defines another

    if (typeof preferredRegion !== 'undefined') {
      config.preferredRegion = preferredRegion
    }

    if (typeof dynamic !== 'undefined') {
      config.dynamic = dynamic
    }

    if (typeof fetchCache !== 'undefined') {
      config.fetchCache = fetchCache
    }

    if (typeof revalidate !== 'undefined') {
      config.revalidate = revalidate
    }

    // Any revalidate number overrides false, and shorter revalidate overrides
    // longer (initially).
    if (
      typeof revalidate === 'number' &&
      (typeof config.revalidate !== 'number' || revalidate < config.revalidate)
    ) {
      config.revalidate = revalidate
    }

    if (typeof runtime !== 'undefined') {
      config.runtime = runtime
    }

    if (typeof maxDuration !== 'undefined') {
      config.maxDuration = maxDuration
    }
  }

  return config
}

export async function hasCustomGetInitialProps({
  page,
  distDir,
  checkingApp,
  sriEnabled,
}: {
  page: string
  distDir: string
  checkingApp: boolean
  sriEnabled: boolean
}): Promise<boolean> {
  const { ComponentMod } = await loadComponents({
    distDir,
    page: page,
    isAppPath: false,
    isDev: false,
    sriEnabled,
    needsManifestsForLegacyReasons: true,
  })
  let mod = ComponentMod

  if (checkingApp) {
    // @ts-expect-error very dynamic code
    mod = (await mod._app) || mod.default || mod
  } else {
    // @ts-expect-error very dynamic code
    mod = mod.default || mod
  }
  mod = await mod
  // @ts-expect-error very dynamic code
  return mod.getInitialProps !== mod.origGetInitialProps
}

export async function getDefinedNamedExports({
  page,
  distDir,
  sriEnabled,
}: {
  page: string
  distDir: string
  sriEnabled: boolean
}): Promise<ReadonlyArray<string>> {
  const { ComponentMod } = await loadComponents({
    distDir,
    page: page,
    isAppPath: false,
    isDev: false,
    sriEnabled,
    needsManifestsForLegacyReasons: true,
  })

  return Object.keys(ComponentMod).filter((key) => {
    return typeof ComponentMod[key as keyof typeof ComponentMod] !== 'undefined'
  })
}

export function detectConflictingPaths(
  combinedPages: string[],
  ssgPages: Set<string>,
  additionalGeneratedSSGPaths: Map<string, string[]>
) {
  const conflictingPaths = new Map<
    string,
    Array<{
      path: string
      page: string
    }>
  >()

  const dynamicSsgPages = [...ssgPages].filter((page) => isDynamicRoute(page))
  const additionalSsgPathsByPath: {
    [page: string]: { [path: string]: string }
  } = {}

  additionalGeneratedSSGPaths.forEach((paths, pathsPage) => {
    additionalSsgPathsByPath[pathsPage] ||= {}
    paths.forEach((curPath) => {
      const currentPath = curPath.toLowerCase()
      additionalSsgPathsByPath[pathsPage][currentPath] = curPath
    })
  })

  additionalGeneratedSSGPaths.forEach((paths, pathsPage) => {
    paths.forEach((curPath) => {
      const lowerPath = curPath.toLowerCase()
      let conflictingPage = combinedPages.find(
        (page) => page.toLowerCase() === lowerPath
      )

      if (conflictingPage) {
        conflictingPaths.set(lowerPath, [
          { path: curPath, page: pathsPage },
          { path: conflictingPage, page: conflictingPage },
        ])
      } else {
        let conflictingPath: string | undefined

        conflictingPage = dynamicSsgPages.find((page) => {
          if (page === pathsPage) return false

          conflictingPath =
            additionalGeneratedSSGPaths.get(page) == null
              ? undefined
              : additionalSsgPathsByPath[page][lowerPath]
          return conflictingPath
        })

        if (conflictingPage && conflictingPath) {
          conflictingPaths.set(lowerPath, [
            { path: curPath, page: pathsPage },
            { path: conflictingPath, page: conflictingPage },
          ])
        }
      }
    })
  })

  if (conflictingPaths.size > 0) {
    let conflictingPathsOutput = ''

    conflictingPaths.forEach((pathItems) => {
      pathItems.forEach((pathItem, idx) => {
        const isDynamic = pathItem.page !== pathItem.path

        if (idx > 0) {
          conflictingPathsOutput += 'conflicts with '
        }

        conflictingPathsOutput += `path: "${pathItem.path}"${
          isDynamic ? ` from page: "${pathItem.page}" ` : ' '
        }`
      })
      conflictingPathsOutput += '\n'
    })

    Log.error(
      'Conflicting paths returned from getStaticPaths, paths must be unique per page.\n' +
        'See more info here: https://nextjs.org/docs/messages/conflicting-ssg-paths\n\n' +
        conflictingPathsOutput
    )
    process.exit(1)
  }
}

export async function copyTracedFiles(
  dir: string,
  distDir: string,
  pageKeys: readonly string[],
  appPageKeys: readonly string[] | undefined,
  tracingRoot: string,
  serverConfig: NextConfigRuntime,
  middlewareManifest: MiddlewareManifest,
  hasNodeMiddleware: boolean,
  hasInstrumentationHook: boolean,
  staticPages: Set<string>
) {
  const outputPath = path.join(distDir, 'standalone')

  // Clean up standalone directory first.
  await fs.rm(outputPath, { recursive: true, force: true })

  let moduleType = false
  const nextConfig = {
    ...serverConfig,
    distDir: `./${path.relative(dir, distDir)}`,
  }
  try {
    const packageJsonPath = path.join(distDir, '../package.json')
    const packageJsonContent = await fs.readFile(
      /* turbopackIgnore: true */ packageJsonPath,
      'utf8'
    )
    const packageJson = JSON.parse(packageJsonContent)
    moduleType = packageJson.type === 'module'

    // we always copy the package.json to the standalone
    // folder to ensure any resolving logic is maintained
    const packageJsonOutputPath = path.join(
      outputPath,
      path.relative(tracingRoot, dir),
      'package.json'
    )
    await fs.mkdir(path.dirname(packageJsonOutputPath), { recursive: true })
    await fs.writeFile(packageJsonOutputPath, packageJsonContent)
  } catch {}
  const copiedFiles = new Set()

  async function handleTraceFiles(traceFilePath: string) {
    const traceData = JSON.parse(
      await fs.readFile(/* turbopackIgnore: true */ traceFilePath, 'utf8')
    ) as {
      files: string[]
    }
    const copySema = new Sema(10, { capacity: traceData.files.length })
    const traceFileDir = path.dirname(traceFilePath)

    await Promise.all(
      traceData.files.map(async (relativeFile) => {
        await copySema.acquire()

        const tracedFilePath = path.join(traceFileDir, relativeFile)
        const fileOutputPath = path.join(
          outputPath,
          path.relative(tracingRoot, tracedFilePath)
        )

        if (!copiedFiles.has(fileOutputPath)) {
          copiedFiles.add(fileOutputPath)

          await fs.mkdir(path.dirname(fileOutputPath), { recursive: true })
          const symlink = await fs.readlink(tracedFilePath).catch(() => null)

          if (symlink) {
            try {
              await fs.symlink(symlink, fileOutputPath)
            } catch (err: any) {
              // Windows doesn't support creating symlinks without elevated privileges, unless
              // "Developer Mode" is turned on. If we failed to create a symlink due to EPERM, try
              // creating a junction point instead.
              //
              // Ideally we'd just preserve the input file type (junction point or symlink), but
              // there's no API in node.js to differentiate between a junction point and a symlink,
              // so we just try making a symlink first. Symlinks are preferred because they support
              // relative paths and non-directory (file) targets.
              if (
                process.platform === 'win32' &&
                err.code === 'EPERM' &&
                path.isAbsolute(symlink)
              ) {
                try {
                  await fs.symlink(symlink, fileOutputPath, 'junction')
                } catch (junctionErr: any) {
                  if (junctionErr.code !== 'EEXIST') {
                    throw junctionErr
                  }
                }
              } else if (err.code !== 'EEXIST') {
                throw err
              }
            }
          } else {
            await fs.copyFile(tracedFilePath, fileOutputPath)
          }
        }

        await copySema.release()
      })
    )
  }

  async function handleEdgeFunction(page: EdgeFunctionDefinition) {
    async function handleFile(file: string) {
      const originalPath = path.join(distDir, file)
      const fileOutputPath = path.join(
        outputPath,
        path.relative(tracingRoot, distDir),
        file
      )
      await fs.mkdir(path.dirname(fileOutputPath), { recursive: true })
      await fs.copyFile(originalPath, fileOutputPath)
    }
    await Promise.all([
      page.files.map(handleFile),
      page.wasm?.map((file) => handleFile(file.filePath)),
      page.assets?.map((file) => handleFile(file.filePath)),
    ])
  }

  const edgeFunctionHandlers: Promise<any>[] = []

  for (const middleware of Object.values(middlewareManifest.middleware)) {
    if (isMiddlewareFilename(middleware.name)) {
      edgeFunctionHandlers.push(handleEdgeFunction(middleware))
    }
  }

  for (const page of Object.values(middlewareManifest.functions)) {
    edgeFunctionHandlers.push(handleEdgeFunction(page))
  }

  await Promise.all(edgeFunctionHandlers)

  for (const page of pageKeys) {
    if (middlewareManifest.functions.hasOwnProperty(page)) {
      continue
    }
    const route = normalizePagePath(page)

    if (staticPages.has(route)) {
      continue
    }

    const pageFile = path.join(
      distDir,
      'server',
      'pages',
      `${normalizePagePath(page)}.js`
    )
    const pageTraceFile = `${pageFile}.nft.json`
    await handleTraceFiles(pageTraceFile).catch((err) => {
      if (err.code !== 'ENOENT' || (page !== '/404' && page !== '/500')) {
        Log.warn(`Failed to copy traced files for ${pageFile}`, err)
      }
    })
  }

  if (hasNodeMiddleware) {
    const middlewareFile = path.join(distDir, 'server', 'middleware.js')
    const middlewareTrace = `${middlewareFile}.nft.json`
    await handleTraceFiles(middlewareTrace)
  }

  if (appPageKeys) {
    for (const page of appPageKeys) {
      if (middlewareManifest.functions.hasOwnProperty(page)) {
        continue
      }
      const pageFile = path.join(distDir, 'server', 'app', `${page}.js`)
      const pageTraceFile = `${pageFile}.nft.json`
      await handleTraceFiles(pageTraceFile).catch((err) => {
        Log.warn(`Failed to copy traced files for ${pageFile}`, err)
      })
    }
  }

  if (hasInstrumentationHook) {
    await handleTraceFiles(
      path.join(distDir, 'server', 'instrumentation.js.nft.json')
    )
  }

  await handleTraceFiles(path.join(distDir, 'next-server.js.nft.json'))
  const serverOutputPath = path.join(
    outputPath,
    path.relative(tracingRoot, dir),
    'server.js'
  )
  await fs.mkdir(path.dirname(serverOutputPath), { recursive: true })

  await fs.writeFile(
    serverOutputPath,
    `${
      moduleType
        ? `performance.mark('next-start');
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import module from 'node:module'
const require = module.createRequire(import.meta.url)
const __dirname = fileURLToPath(new URL('.', import.meta.url))
`
        : `const path = require('path')`
    }

const dir = path.join(__dirname)

process.env.NODE_ENV = 'production'
process.chdir(__dirname)

const currentPort = parseInt(process.env.PORT, 10) || 3000
const hostname = process.env.HOSTNAME || '0.0.0.0'

let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
const nextConfig = ${JSON.stringify(nextConfig)}

process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)

require('next')
const { startServer } = require('next/dist/server/lib/start-server')

if (
  Number.isNaN(keepAliveTimeout) ||
  !Number.isFinite(keepAliveTimeout) ||
  keepAliveTimeout < 0
) {
  keepAliveTimeout = undefined
}

startServer({
  dir,
  isDev: false,
  config: nextConfig,
  hostname,
  port: currentPort,
  allowRetry: false,
  keepAliveTimeout,
}).catch((err) => {
  console.error(err);
  process.exit(1);
});`
  )
}

export function isReservedPage(page: string) {
  return RESERVED_PAGE.test(page)
}

export function isAppBuiltinPage(page: string) {
  return /next[\\/]dist[\\/](esm[\\/])?client[\\/]components[\\/]builtin[\\/]/.test(
    page
  )
}

export function isCustomErrorPage(page: string) {
  return page === '/404' || page === '/500'
}

export function isMiddlewareFile(file: string) {
  return (
    file === `/${MIDDLEWARE_FILENAME}` ||
    file === `/src/${MIDDLEWARE_FILENAME}` ||
    file === `/${PROXY_FILENAME}` ||
    file === `/src/${PROXY_FILENAME}`
  )
}

export function isProxyFile(file: string) {
  return file === `/${PROXY_FILENAME}` || file === `/src/${PROXY_FILENAME}`
}

export function isInstrumentationHookFile(file: string) {
  return (
    file === `/${INSTRUMENTATION_HOOK_FILENAME}` ||
    file === `/src/${INSTRUMENTATION_HOOK_FILENAME}`
  )
}

export function getPossibleInstrumentationHookFilenames(
  folder: string,
  extensions: string[]
) {
  const files = []
  for (const extension of extensions) {
    files.push(
      path.join(folder, `${INSTRUMENTATION_HOOK_FILENAME}.${extension}`),
      path.join(folder, `src`, `${INSTRUMENTATION_HOOK_FILENAME}.${extension}`)
    )
  }

  return files
}

export function getPossibleMiddlewareFilenames(
  folder: string,
  extensions: string[]
) {
  return extensions.flatMap((extension) => [
    path.join(folder, `${MIDDLEWARE_FILENAME}.${extension}`),
    path.join(folder, `${PROXY_FILENAME}.${extension}`),
  ])
}

export class NestedMiddlewareError extends Error {
  constructor(
    nestedFileNames: string[],
    mainDir: string,
    pagesOrAppDir: string
  ) {
    super(
      `Nested Middleware is not allowed, found:\n` +
        `${nestedFileNames.map((file) => `pages${file}`).join('\n')}\n` +
        `Please move your code to a single file at ${path.join(
          path.posix.sep,
          path.relative(mainDir, path.resolve(pagesOrAppDir, '..')),
          'middleware'
        )} instead.\n` +
        `Read More - https://nextjs.org/docs/messages/nested-middleware`
    )
  }
}

export { getSupportedBrowsers } from './get-supported-browsers'

export function shouldUseReactServerCondition(
  layer: WebpackLayerName | null | undefined
): boolean {
  return Boolean(
    layer && WEBPACK_LAYERS.GROUP.serverOnly.includes(layer as any)
  )
}

export function isWebpackClientOnlyLayer(
  layer: WebpackLayerName | null | undefined
): boolean {
  return Boolean(
    layer && WEBPACK_LAYERS.GROUP.clientOnly.includes(layer as any)
  )
}

export function isWebpackDefaultLayer(
  layer: WebpackLayerName | null | undefined
): boolean {
  return (
    layer === null ||
    layer === undefined ||
    layer === WEBPACK_LAYERS.pagesDirBrowser ||
    layer === WEBPACK_LAYERS.pagesDirEdge ||
    layer === WEBPACK_LAYERS.pagesDirNode
  )
}

export function isWebpackBundledLayer(
  layer: WebpackLayerName | null | undefined
): boolean {
  return Boolean(layer && WEBPACK_LAYERS.GROUP.bundled.includes(layer as any))
}

export function isWebpackAppPagesLayer(
  layer: WebpackLayerName | null | undefined
): boolean {
  return Boolean(layer && WEBPACK_LAYERS.GROUP.appPages.includes(layer as any))
}

export function collectMeta({
  status,
  headers,
}: {
  status?: number
  headers?: OutgoingHttpHeaders
}): {
  status?: number
  headers?: Record<string, string>
} {
  const meta: {
    status?: number
    headers?: Record<string, string>
  } = {}

  if (status !== 200) {
    meta.status = status
  }

  if (headers && Object.keys(headers).length) {
    meta.headers = {}

    // normalize header values as initialHeaders
    // must be Record<string, string>
    for (const key in headers) {
      // set-cookie is already handled - the middleware cookie setting case
      // isn't needed for the prerender manifest since it can't read cookies
      if (key === 'x-middleware-set-cookie') continue

      let value = headers[key]

      if (Array.isArray(value)) {
        if (key === 'set-cookie') {
          value = value.join(',')
        } else {
          value = value[value.length - 1]
        }
      }

      if (typeof value === 'string') {
        meta.headers[key] = value
      }
    }
  }

  return meta
}

export const RSPACK_DEFAULT_LAYERS_REGEX = new RegExp(
  `^(|${[WEBPACK_LAYERS.pagesDirBrowser, WEBPACK_LAYERS.pagesDirEdge, WEBPACK_LAYERS.pagesDirNode].join('|')})$`
)

/**
 * Converts a page to a manifest route.
 *
 * @param page The page to convert to a route.
 * @returns A route object.
 */
export function pageToRoute(page: string): ManifestRoute
/**
 * Converts a page to a dynamic manifest route.
 *
 * @param page The page to convert to a route.
 * @param sourcePage The source page that this route is based on. This is used
 * to determine the source page for the route and is only relevant for app
 * pages when PPR is enabled on them.
 * @returns A route object.
 */
export function pageToRoute(
  page: string,
  sourcePage: string | undefined
): DynamicManifestRoute
export function pageToRoute(
  page: string,
  sourcePage?: string
): DynamicManifestRoute | ManifestRoute {
  const routeRegex = getNamedRouteRegex(page, {
    prefixRouteKeys: true,
  })
  return {
    sourcePage,
    page,
    regex: normalizeRouteRegex(routeRegex.re.source),
    routeKeys: routeRegex.routeKeys,
    namedRegex: routeRegex.namedRegex,
  }
}
Quest for Codev2.0.0
/
SIGN IN