next.js/packages/next/src/build/entries.ts
entries.ts774 lines23.3 KB
import type { ClientPagesLoaderOptions } from './webpack/loaders/next-client-pages-loader'
import type { MiddlewareLoaderOptions } from './webpack/loaders/next-middleware-loader'
import type { EdgeSSRLoaderQuery } from './webpack/loaders/next-edge-ssr-loader'
import type { EdgeAppRouteLoaderQuery } from './webpack/loaders/next-edge-app-route-loader'
import type { NextConfigComplete } from '../server/config-shared'
import type { webpack } from 'next/dist/compiled/webpack/webpack'
import type {
  ProxyConfig,
  ProxyMatcher,
  PageStaticInfo,
} from './analysis/get-page-static-info'
import type { LoadedEnvFiles } from '@next/env'
import type { AppLoaderOptions } from './webpack/loaders/next-app-loader'

import { posix, join, normalize } from 'path'
import { stringify } from 'querystring'
import {
  PAGES_DIR_ALIAS,
  ROOT_DIR_ALIAS,
  APP_DIR_ALIAS,
  WEBPACK_LAYERS,
  INSTRUMENTATION_HOOK_FILENAME,
} from '../lib/constants'
import { isAPIRoute } from '../lib/is-api-route'
import { isEdgeRuntime } from '../lib/is-edge-runtime'
import { APP_CLIENT_INTERNALS, RSC_MODULE_TYPES } from '../shared/lib/constants'
import {
  CLIENT_STATIC_FILES_RUNTIME_MAIN,
  CLIENT_STATIC_FILES_RUNTIME_MAIN_APP,
  CLIENT_STATIC_FILES_RUNTIME_POLYFILLS,
  CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH,
  COMPILER_NAMES,
  EDGE_RUNTIME_WEBPACK,
} from '../shared/lib/constants'
import type { CompilerNameValues } from '../shared/lib/constants'
import type { __ApiPreviewProps } from '../server/api-utils'
import {
  isMiddlewareFile,
  isMiddlewareFilename,
  isProxyFile,
  isInstrumentationHookFile,
  isInstrumentationHookFilename,
} from './utils'
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import type { ServerRuntime } from '../types'
import {
  normalizeAppPath,
  compareAppPaths,
} from '../shared/lib/router/utils/app-paths'
import { encodeMatchers } from './webpack/loaders/next-middleware-loader'
import type { EdgeFunctionLoaderOptions } from './webpack/loaders/next-edge-function-loader'
import { isAppRouteRoute } from '../lib/is-app-route-route'
import { getRouteLoaderEntry } from './webpack/loaders/next-route-loader'
import {
  isInternalComponent,
  isNonRoutePagesPage,
} from '../lib/is-internal-component'
import { RouteKind } from '../server/route-kind'
import { encodeToBase64 } from './webpack/loaders/utils'
import { normalizeCatchAllRoutes } from './normalize-catchall-routes'
import type { PageExtensions } from './page-extensions-type'
import type { MappedPages } from './build-context'
import { PAGE_TYPES } from '../lib/page-types'

type ObjectValue<T> = T extends { [key: string]: infer V } ? V : never
import { getStaticInfoIncludingLayouts } from './get-static-info-including-layouts'
import { getPageFromPath } from './route-discovery'

export function getPageFilePath({
  absolutePagePath,
  pagesDir,
  appDir,
  rootDir,
}: {
  absolutePagePath: string
  pagesDir: string | undefined
  appDir: string | undefined
  rootDir: string
}) {
  if (absolutePagePath.startsWith(PAGES_DIR_ALIAS) && pagesDir) {
    return absolutePagePath.replace(PAGES_DIR_ALIAS, pagesDir)
  }

  if (absolutePagePath.startsWith(APP_DIR_ALIAS) && appDir) {
    return absolutePagePath.replace(APP_DIR_ALIAS, appDir)
  }

  if (absolutePagePath.startsWith(ROOT_DIR_ALIAS)) {
    return absolutePagePath.replace(ROOT_DIR_ALIAS, rootDir)
  }

  return require.resolve(absolutePagePath)
}

export interface CreateEntrypointsParams {
  buildId: string
  config: NextConfigComplete
  envFiles: LoadedEnvFiles
  isDev: boolean
  pages: MappedPages
  pagesDir?: string
  previewMode: __ApiPreviewProps
  rootDir: string
  rootPaths?: MappedPages
  appDir?: string
  appPaths?: MappedPages
  pageExtensions: PageExtensions
  hasInstrumentationHook?: boolean
  /**
   * When set to 'exclude', deferred entries are excluded from the result.
   * When set to 'only', only deferred entries are included in the result.
   * When undefined, all entries are included.
   */
  deferredEntriesFilter?: 'exclude' | 'only'
}

/**
 * Checks if a page path matches any of the deferred entry patterns.
 * @param page - The page path (e.g., '/about', '/api/hello')
 * @param deferredEntries - Array of path patterns to match against
 * @returns true if the page matches a deferred entry pattern
 */
export function isDeferredEntry(
  page: string,
  deferredEntries: string[] | undefined
): boolean {
  if (!deferredEntries || deferredEntries.length === 0) {
    return false
  }

  // Normalize the page path
  const normalizedPage = page.startsWith('/') ? page : `/${page}`

  for (const pattern of deferredEntries) {
    // Normalize the pattern
    const normalizedPattern = pattern.startsWith('/') ? pattern : `/${pattern}`

    // Check for exact match or prefix match for directories
    if (normalizedPage === normalizedPattern) {
      return true
    }

    // Check if the page is under the deferred directory
    if (normalizedPage.startsWith(normalizedPattern + '/')) {
      return true
    }
  }

  return false
}

export function getEdgeServerEntry(opts: {
  rootDir: string
  absolutePagePath: string
  buildId: string
  bundlePath: string
  config: NextConfigComplete
  isDev: boolean
  isServerComponent: boolean
  page: string
  pages: MappedPages
  middleware?: Partial<ProxyConfig>
  pagesType: PAGE_TYPES
  appDirLoader?: string
  hasInstrumentationHook?: boolean
  preferredRegion: string | string[] | undefined
  middlewareConfig?: ProxyConfig
}) {
  const cacheHandler = opts.config.cacheHandler || undefined

  if (
    opts.pagesType === 'app' &&
    isAppRouteRoute(opts.page) &&
    opts.appDirLoader
  ) {
    const loaderParams: EdgeAppRouteLoaderQuery = {
      absolutePagePath: opts.absolutePagePath,
      page: opts.page,
      appDirLoader: Buffer.from(opts.appDirLoader || '').toString('base64'),
      preferredRegion: opts.preferredRegion,
      middlewareConfig: Buffer.from(
        JSON.stringify(opts.middlewareConfig || {})
      ).toString('base64'),
      cacheHandlers: JSON.stringify(opts.config.cacheHandlers || {}),
      ...(cacheHandler ? { cacheHandler } : {}),
    }

    return {
      import: `next-edge-app-route-loader?${stringify(loaderParams)}!`,
      layer: WEBPACK_LAYERS.reactServerComponents,
    }
  }

  if (isMiddlewareFile(opts.page)) {
    const loaderParams: MiddlewareLoaderOptions = {
      absolutePagePath: opts.absolutePagePath,
      page: opts.page,
      rootDir: opts.rootDir,
      matchers: opts.middleware?.matchers
        ? encodeMatchers(opts.middleware.matchers)
        : '',
      preferredRegion: opts.preferredRegion,
      middlewareConfig: Buffer.from(
        JSON.stringify(opts.middlewareConfig || {})
      ).toString('base64'),
      ...(cacheHandler ? { cacheHandler } : {}),
    }

    return {
      import: `next-middleware-loader?${stringify(loaderParams)}!`,
      layer: WEBPACK_LAYERS.middleware,
      filename: opts.isDev ? 'middleware.js' : undefined,
    }
  }

  if (isAPIRoute(opts.page)) {
    const loaderParams: EdgeFunctionLoaderOptions = {
      absolutePagePath: opts.absolutePagePath,
      page: opts.page,
      rootDir: opts.rootDir,
      preferredRegion: opts.preferredRegion,
      middlewareConfig: Buffer.from(
        JSON.stringify(opts.middlewareConfig || {})
      ).toString('base64'),
      ...(cacheHandler ? { cacheHandler } : {}),
    }

    return {
      import: `next-edge-function-loader?${stringify(loaderParams)}!`,
      layer: WEBPACK_LAYERS.apiEdge,
    }
  }

  const loaderParams: EdgeSSRLoaderQuery = {
    absolute500Path: opts.pages['/500'] || '',
    absoluteAppPath: opts.pages['/_app'],
    absoluteDocumentPath: opts.pages['/_document'],
    absoluteErrorPath: opts.pages['/_error'],
    absolutePagePath: opts.absolutePagePath,
    dev: opts.isDev,
    isServerComponent: opts.isServerComponent,
    page: opts.page,
    pagesType: opts.pagesType,
    appDirLoader: Buffer.from(opts.appDirLoader || '').toString('base64'),
    sriEnabled: !opts.isDev && !!opts.config.experimental.sri?.algorithm,
    preferredRegion: opts.preferredRegion,
    middlewareConfig: Buffer.from(
      JSON.stringify(opts.middlewareConfig || {})
    ).toString('base64'),
    serverActions: opts.config.experimental.serverActions,
    cacheHandlers: JSON.stringify(opts.config.cacheHandlers || {}),
    ...(cacheHandler ? { cacheHandler } : {}),
  }

  return {
    import: `next-edge-ssr-loader?${JSON.stringify(loaderParams)}!`,
    // The Edge bundle includes the server in its entrypoint, so it has to
    // be in the SSR layer — we later convert the page request to the RSC layer
    // via a webpack rule.
    layer: opts.appDirLoader ? WEBPACK_LAYERS.serverSideRendering : undefined,
  }
}

export function getInstrumentationEntry(opts: {
  absolutePagePath: string
  isEdgeServer: boolean
  isDev: boolean
}) {
  // the '../' is needed to make sure the file is not chunked
  const filename = `${
    opts.isEdgeServer ? 'edge-' : opts.isDev ? '' : '../'
  }${INSTRUMENTATION_HOOK_FILENAME}.js`

  return {
    import: opts.absolutePagePath,
    filename,
    layer: WEBPACK_LAYERS.instrument,
  }
}

export function getAppLoader() {
  return process.env.BUILTIN_APP_LOADER
    ? `builtin:next-app-loader`
    : 'next-app-loader'
}

export function getAppEntry(opts: Readonly<AppLoaderOptions>) {
  if (process.env.NEXT_RSPACK && process.env.BUILTIN_APP_LOADER) {
    ;(opts as any).projectRoot = normalize(join(__dirname, '../../..'))
  }
  return {
    import: `${getAppLoader()}?${stringify(opts)}!`,
    layer: WEBPACK_LAYERS.reactServerComponents,
  }
}

export function getClientEntry(opts: {
  absolutePagePath: string
  page: string
}) {
  const loaderOptions: ClientPagesLoaderOptions = {
    absolutePagePath: opts.absolutePagePath,
    page: opts.page,
  }

  const pageLoader = `next-client-pages-loader?${stringify(loaderOptions)}!`

  // Make sure next/router is a dependency of _app or else chunk splitting
  // might cause the router to not be able to load causing hydration
  // to fail
  return opts.page === '/_app'
    ? [pageLoader, require.resolve('../client/router')]
    : pageLoader
}

export function runDependingOnPageType<T>(params: {
  onClient: () => T
  onEdgeServer: () => T
  onServer: () => T
  page: string
  pageRuntime: ServerRuntime
  pageType?: PAGE_TYPES
}): void {
  if (
    params.pageType === PAGE_TYPES.ROOT &&
    isInstrumentationHookFile(params.page)
  ) {
    params.onServer()
    params.onEdgeServer()
    return
  }

  if (isProxyFile(params.page)) {
    params.onServer()
    return
  }

  if (isMiddlewareFile(params.page)) {
    if (params.pageRuntime === 'nodejs') {
      params.onServer()
      return
    } else {
      params.onEdgeServer()
      return
    }
  }

  if (isAPIRoute(params.page)) {
    if (isEdgeRuntime(params.pageRuntime)) {
      params.onEdgeServer()
      return
    }

    params.onServer()
    return
  }
  if (params.page === '/_document') {
    params.onServer()
    return
  }
  if (
    params.page === '/_app' ||
    params.page === '/_error' ||
    params.page === '/404' ||
    params.page === '/500'
  ) {
    params.onClient()
    params.onServer()
    return
  }
  if (isEdgeRuntime(params.pageRuntime)) {
    params.onClient()
    params.onEdgeServer()
    return
  }

  params.onClient()
  params.onServer()
  return
}

export async function createEntrypoints(
  params: CreateEntrypointsParams
): Promise<{
  client: webpack.EntryObject
  server: webpack.EntryObject
  edgeServer: webpack.EntryObject
  middlewareMatchers: undefined
}> {
  const {
    config,
    pages,
    pagesDir,
    isDev,
    rootDir,
    rootPaths,
    appDir,
    appPaths,
    pageExtensions,
    deferredEntriesFilter,
  } = params

  const deferredEntries = config.experimental.deferredEntries
  const edgeServer: webpack.EntryObject = {}
  const server: webpack.EntryObject = {}
  const client: webpack.EntryObject = {}
  let middlewareMatchers: ProxyMatcher[] | undefined = undefined

  let appPathsPerRoute: Record<string, string[]> = {}
  if (appDir && appPaths) {
    for (const pathname in appPaths) {
      const normalizedPath = normalizeAppPath(pathname)
      const actualPath = appPaths[pathname]
      if (!appPathsPerRoute[normalizedPath]) {
        appPathsPerRoute[normalizedPath] = []
      }
      appPathsPerRoute[normalizedPath].push(
        // TODO-APP: refactor to pass the page path from createPagesMapping instead.
        getPageFromPath(actualPath, pageExtensions).replace(APP_DIR_ALIAS, '')
      )
    }

    // TODO: find a better place to do this
    normalizeCatchAllRoutes(appPathsPerRoute)

    // Make sure to sort parallel routes to make the result deterministic.
    appPathsPerRoute = Object.fromEntries(
      Object.entries(appPathsPerRoute).map(([k, v]) => [
        k,
        v.sort(compareAppPaths),
      ])
    )
  }

  const getEntryHandler =
    (mappings: MappedPages, pagesType: PAGE_TYPES): ((page: string) => void) =>
    async (page) => {
      // Apply deferred entries filter if specified
      if (deferredEntriesFilter) {
        const isDeferred = isDeferredEntry(page, deferredEntries)
        if (deferredEntriesFilter === 'exclude' && isDeferred) {
          // Skip deferred entries when excluding them
          return
        }
        if (deferredEntriesFilter === 'only' && !isDeferred) {
          // Skip non-deferred entries when only including deferred ones
          return
        }
      }

      const bundleFile = normalizePagePath(page)
      const clientBundlePath = posix.join(pagesType, bundleFile)
      const serverBundlePath =
        pagesType === PAGE_TYPES.PAGES
          ? posix.join('pages', bundleFile)
          : pagesType === PAGE_TYPES.APP
            ? posix.join('app', bundleFile)
            : bundleFile.slice(1)

      const absolutePagePath = mappings[page]

      // Handle paths that have aliases
      const pageFilePath = getPageFilePath({
        absolutePagePath,
        pagesDir,
        appDir,
        rootDir,
      })

      const isInsideAppDir =
        !!appDir &&
        (absolutePagePath.startsWith(APP_DIR_ALIAS) ||
          absolutePagePath.startsWith(appDir))

      const staticInfo: PageStaticInfo = await getStaticInfoIncludingLayouts({
        isInsideAppDir,
        pageExtensions,
        pageFilePath,
        appDir,
        config,
        isDev,
        page,
      })

      // TODO(timneutkens): remove this
      const isServerComponent =
        isInsideAppDir && staticInfo.rsc !== RSC_MODULE_TYPES.client

      if (isMiddlewareFile(page)) {
        middlewareMatchers = staticInfo.middleware?.matchers ?? [
          { regexp: '.*', originalSource: '/:path*' },
        ]
      }

      const isInstrumentation =
        isInstrumentationHookFile(page) && pagesType === PAGE_TYPES.ROOT

      runDependingOnPageType({
        page,
        pageRuntime: staticInfo.runtime,
        pageType: pagesType,
        onClient: () => {
          if (isServerComponent || isInsideAppDir) {
            // We skip the initial entries for server component pages and let the
            // server compiler inject them instead.
          } else {
            client[clientBundlePath] = getClientEntry({
              absolutePagePath,
              page,
            })
          }
        },
        onServer: () => {
          if (pagesType === 'app' && appDir) {
            const matchedAppPaths = appPathsPerRoute[normalizeAppPath(page)]
            server[serverBundlePath] = getAppEntry({
              page,
              name: serverBundlePath,
              pagePath: absolutePagePath,
              appDir,
              appPaths: matchedAppPaths,
              allNormalizedAppPaths: Object.keys(appPathsPerRoute),
              pageExtensions,
              basePath: config.basePath,
              assetPrefix: config.assetPrefix,
              nextConfigOutput: config.output,
              preferredRegion: staticInfo.preferredRegion,
              middlewareConfig: encodeToBase64(staticInfo.middleware || {}),
              isGlobalNotFoundEnabled: config.experimental.globalNotFound
                ? true
                : undefined,
            })
          } else if (isInstrumentation) {
            server[serverBundlePath.replace('src/', '')] =
              getInstrumentationEntry({
                absolutePagePath,
                isEdgeServer: false,
                isDev: false,
              })
          } else if (isMiddlewareFile(page)) {
            server[serverBundlePath.replace('src/', '')] = getEdgeServerEntry({
              ...params,
              rootDir,
              absolutePagePath: absolutePagePath,
              bundlePath: clientBundlePath,
              isDev: false,
              isServerComponent,
              page,
              middleware: staticInfo?.middleware,
              pagesType,
              preferredRegion: staticInfo.preferredRegion,
              middlewareConfig: staticInfo.middleware,
            })
          } else if (isAPIRoute(page)) {
            server[serverBundlePath] = [
              getRouteLoaderEntry({
                kind: RouteKind.PAGES_API,
                page,
                absolutePagePath,
                preferredRegion: staticInfo.preferredRegion,
                middlewareConfig: staticInfo.middleware || {},
              }),
            ]
          } else if (
            !isMiddlewareFile(page) &&
            !isInternalComponent(absolutePagePath) &&
            !isNonRoutePagesPage(page)
          ) {
            server[serverBundlePath] = [
              getRouteLoaderEntry({
                kind: RouteKind.PAGES,
                page,
                pages,
                absolutePagePath,
                preferredRegion: staticInfo.preferredRegion,
                middlewareConfig: staticInfo.middleware ?? {},
              }),
            ]
          } else {
            server[serverBundlePath] = [absolutePagePath]
          }
        },
        onEdgeServer: () => {
          let appDirLoader: string = ''
          if (isInstrumentation) {
            edgeServer[serverBundlePath.replace('src/', '')] =
              getInstrumentationEntry({
                absolutePagePath,
                isEdgeServer: true,
                isDev: false,
              })
          } else {
            if (pagesType === 'app') {
              const matchedAppPaths = appPathsPerRoute[normalizeAppPath(page)]
              appDirLoader = getAppEntry({
                name: serverBundlePath,
                page,
                pagePath: absolutePagePath,
                appDir: appDir!,
                appPaths: matchedAppPaths,
                allNormalizedAppPaths: Object.keys(appPathsPerRoute),
                pageExtensions,
                basePath: config.basePath,
                assetPrefix: config.assetPrefix,
                nextConfigOutput: config.output,
                // This isn't used with edge as it needs to be set on the entry module, which will be the `edgeServerEntry` instead.
                // Still passing it here for consistency.
                preferredRegion: staticInfo.preferredRegion,
                middlewareConfig: Buffer.from(
                  JSON.stringify(staticInfo.middleware || {})
                ).toString('base64'),
                isGlobalNotFoundEnabled: config.experimental.globalNotFound
                  ? true
                  : undefined,
              }).import
            }
            edgeServer[serverBundlePath] = getEdgeServerEntry({
              ...params,
              rootDir,
              absolutePagePath: absolutePagePath,
              bundlePath: clientBundlePath,
              isDev: false,
              isServerComponent,
              page,
              middleware: staticInfo?.middleware,
              pagesType,
              appDirLoader,
              preferredRegion: staticInfo.preferredRegion,
              middlewareConfig: staticInfo.middleware,
            })
          }
        },
      })
    }

  const promises: Promise<void[]>[] = []

  if (appPaths) {
    const entryHandler = getEntryHandler(appPaths, PAGE_TYPES.APP)
    promises.push(Promise.all(Object.keys(appPaths).map(entryHandler)))
  }
  if (rootPaths) {
    promises.push(
      Promise.all(
        Object.keys(rootPaths).map(getEntryHandler(rootPaths, PAGE_TYPES.ROOT))
      )
    )
  }
  promises.push(
    Promise.all(
      Object.keys(pages).map(getEntryHandler(pages, PAGE_TYPES.PAGES))
    )
  )

  await Promise.all(promises)

  // Optimization: If there's only one instrumentation hook in edge compiler, which means there's no edge server entry.
  // We remove the edge instrumentation entry from edge compiler as it can be pure server side.
  if (edgeServer.instrumentation && Object.keys(edgeServer).length === 1) {
    delete edgeServer.instrumentation
  }

  return {
    client,
    server,
    edgeServer,
    middlewareMatchers,
  }
}

export function finalizeEntrypoint({
  name,
  compilerType,
  value,
  isServerComponent,
  hasAppDir,
}: {
  compilerType: CompilerNameValues
  name: string
  value: ObjectValue<webpack.EntryObject>
  isServerComponent?: boolean
  hasAppDir?: boolean
}): ObjectValue<webpack.EntryObject> {
  const entry =
    typeof value !== 'object' || Array.isArray(value)
      ? { import: value }
      : value

  const isApi = name.startsWith('pages/api/')
  const isInstrumentation = isInstrumentationHookFilename(name)

  switch (compilerType) {
    case COMPILER_NAMES.server: {
      const layer = isApi
        ? WEBPACK_LAYERS.apiNode
        : isInstrumentation
          ? WEBPACK_LAYERS.instrument
          : isServerComponent
            ? WEBPACK_LAYERS.reactServerComponents
            : name.startsWith('pages/')
              ? WEBPACK_LAYERS.pagesDirNode
              : undefined

      return {
        publicPath: isApi ? '' : undefined,
        runtime: isApi ? 'webpack-api-runtime' : 'webpack-runtime',
        layer,
        ...entry,
      }
    }
    case COMPILER_NAMES.edgeServer: {
      return {
        layer: isApi
          ? WEBPACK_LAYERS.apiEdge
          : isMiddlewareFilename(name) || isInstrumentation
            ? WEBPACK_LAYERS.middleware
            : name.startsWith('pages/')
              ? WEBPACK_LAYERS.pagesDirEdge
              : undefined,
        library: { name: ['_ENTRIES', `middleware_[name]`], type: 'assign' },
        runtime: EDGE_RUNTIME_WEBPACK,
        asyncChunks: false,
        ...entry,
      }
    }
    case COMPILER_NAMES.client: {
      const isAppLayer =
        hasAppDir &&
        (name === CLIENT_STATIC_FILES_RUNTIME_MAIN_APP ||
          name === APP_CLIENT_INTERNALS ||
          name.startsWith('app/'))

      if (
        // Client special cases
        name !== CLIENT_STATIC_FILES_RUNTIME_POLYFILLS &&
        name !== CLIENT_STATIC_FILES_RUNTIME_MAIN &&
        name !== CLIENT_STATIC_FILES_RUNTIME_MAIN_APP &&
        name !== CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH
      ) {
        if (isAppLayer) {
          return {
            dependOn: CLIENT_STATIC_FILES_RUNTIME_MAIN_APP,
            layer: WEBPACK_LAYERS.appPagesBrowser,
            ...entry,
          }
        }

        return {
          dependOn:
            name.startsWith('pages/') && name !== 'pages/_app'
              ? 'pages/_app'
              : CLIENT_STATIC_FILES_RUNTIME_MAIN,
          layer: WEBPACK_LAYERS.pagesDirBrowser,
          ...entry,
        }
      }

      if (isAppLayer) {
        return {
          layer: WEBPACK_LAYERS.appPagesBrowser,
          ...entry,
        }
      }

      return {
        layer: WEBPACK_LAYERS.pagesDirBrowser,
        ...entry,
      }
    }
    default:
      return compilerType satisfies never
  }
}
Quest for Codev2.0.0
/
SIGN IN