next.js/packages/next/src/shared/lib/turbopack/manifest-loader.ts
manifest-loader.ts889 lines25.9 KB
import type {
  EdgeFunctionDefinition,
  MiddlewareManifest,
} from '../../../build/webpack/plugins/middleware-plugin'
import type { BuildManifest } from '../../../server/get-page-files'
import type { PagesManifest } from '../../../build/webpack/plugins/pages-manifest-plugin'
import type { ActionManifest } from '../../../build/webpack/plugins/flight-client-entry-plugin'
import type { NextFontManifest } from '../../../build/webpack/plugins/next-font-manifest-plugin'
import type { REACT_LOADABLE_MANIFEST } from '../constants'
import {
  APP_PATHS_MANIFEST,
  BUILD_MANIFEST,
  CLIENT_STATIC_FILES_PATH,
  INTERCEPTION_ROUTE_REWRITE_MANIFEST,
  MIDDLEWARE_BUILD_MANIFEST,
  MIDDLEWARE_MANIFEST,
  NEXT_FONT_MANIFEST,
  PAGES_MANIFEST,
  SERVER_REFERENCE_MANIFEST,
  SUBRESOURCE_INTEGRITY_MANIFEST,
  TURBOPACK_CLIENT_BUILD_MANIFEST,
  TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST,
} from '../constants'
import { join, posix } from 'path'
import { readFileSync } from 'fs'
import type { SetupOpts } from '../../../server/lib/router-utils/setup-dev-bundler'
import { deleteCache } from '../../../server/dev/require-cache'
import { writeFileAtomic } from '../../../lib/fs/write-atomic'
import getAssetPathFromRoute from '../router/utils/get-asset-path-from-route'
import { getEntryKey, type EntryKey } from './entry-key'
import type { CustomRoutes } from '../../../lib/load-custom-routes'
import { getSortedRoutes } from '../router/utils'
import { existsSync } from 'fs'
import {
  addMetadataIdToRoute,
  addRouteSuffix,
  removeRouteSuffix,
} from '../../../server/dev/turbopack-utils'
import { tryToParsePath } from '../../../lib/try-to-parse-path'
import { safePathToRegexp } from '../router/utils/route-match-utils'
import type { Entrypoints } from '../../../build/swc/types'
import {
  normalizeRewritesForBuildManifest,
  type ClientBuildManifest,
  srcEmptySsgManifest,
  processRoute,
  createEdgeRuntimeManifest,
} from '../../../build/webpack/plugins/build-manifest-plugin-utils'
import type { SubresourceIntegrityManifest } from '../../../build'

interface InstrumentationDefinition {
  files: string[]
  name: 'instrumentation'
}

type TurbopackMiddlewareManifest = MiddlewareManifest & {
  instrumentation?: InstrumentationDefinition
}

type ManifestName =
  | typeof MIDDLEWARE_MANIFEST
  | typeof BUILD_MANIFEST
  | typeof PAGES_MANIFEST
  | typeof APP_PATHS_MANIFEST
  | `${typeof SERVER_REFERENCE_MANIFEST}.json`
  | `${typeof SUBRESOURCE_INTEGRITY_MANIFEST}.json`
  | `${typeof NEXT_FONT_MANIFEST}.json`
  | typeof REACT_LOADABLE_MANIFEST
  | typeof TURBOPACK_CLIENT_BUILD_MANIFEST

const getManifestPath = (
  page: string,
  distDir: string,
  name: ManifestName,
  type: string,
  firstCall: boolean
) => {
  let manifestPath = posix.join(
    distDir,
    `server`,
    type,
    type === 'middleware' || type === 'instrumentation'
      ? ''
      : type === 'app'
        ? page
        : getAssetPathFromRoute(page),
    name
  )

  if (firstCall) {
    const isSitemapRoute = /[\\/]sitemap(.xml)?\/route$/.test(page)
    // Check the ambiguity of /sitemap and /sitemap.xml
    if (isSitemapRoute && !existsSync(manifestPath)) {
      manifestPath = getManifestPath(
        page.replace(/\/sitemap\/route$/, '/sitemap.xml/route'),
        distDir,
        name,
        type,
        false
      )
    }
    // existsSync is faster than using the async version
    if (!existsSync(manifestPath) && page.endsWith('/route')) {
      // TODO: Improve implementation of metadata routes, currently it requires this extra check for the variants of the files that can be written.
      let basePage = removeRouteSuffix(page)
      // For sitemap.xml routes with generateSitemaps, the manifest is at
      // /sitemap/[__metadata_id__]/route (without .xml), because the route
      // handler serves at /sitemap/[id] not /sitemap.xml/[id]
      if (basePage.endsWith('/sitemap.xml')) {
        basePage = basePage.slice(0, -'.xml'.length)
      }
      let metadataPage = addRouteSuffix(addMetadataIdToRoute(basePage))
      manifestPath = getManifestPath(metadataPage, distDir, name, type, false)
    }
  }

  return manifestPath
}

function readPartialManifestContent(
  distDir: string,
  name: ManifestName,
  pageName: string,
  type: 'pages' | 'app' | 'middleware' | 'instrumentation' = 'pages'
): string {
  const page = pageName
  const manifestPath = getManifestPath(page, distDir, name, type, true)
  return readFileSync(posix.join(manifestPath), 'utf-8')
}

/// Helper class that stores a map of manifests and tracks if they have changed
/// since the last time they were written to disk. This is used to avoid
/// unnecessary writes to disk.
class ManifestsMap<K, V> {
  private rawMap = new Map<K, string>()
  private map = new Map<K, V>()
  private extraInvalidationKey: string | undefined = undefined
  private changed = true

  set(key: K, value: string) {
    if (this.rawMap.get(key) === value) return
    this.changed = true
    this.rawMap.set(key, value)
    this.map.set(key, JSON.parse(value))
  }

  delete(key: K) {
    if (this.map.has(key)) {
      this.changed = true
      this.rawMap.delete(key)
      this.map.delete(key)
    }
  }

  get(key: K) {
    return this.map.get(key)
  }

  takeChanged(extraInvalidationKey?: any) {
    let changed = this.changed
    if (extraInvalidationKey !== undefined) {
      const stringified = JSON.stringify(extraInvalidationKey)
      if (this.extraInvalidationKey !== stringified) {
        this.extraInvalidationKey = stringified
        changed = true
      }
    }
    this.changed = false
    return changed
  }

  values() {
    return this.map.values()
  }
}

export class TurbopackManifestLoader {
  private actionManifests: ManifestsMap<EntryKey, ActionManifest> =
    new ManifestsMap()
  private appPathsManifests: ManifestsMap<EntryKey, PagesManifest> =
    new ManifestsMap()
  private buildManifests: ManifestsMap<EntryKey, BuildManifest> =
    new ManifestsMap()
  private clientBuildManifests: ManifestsMap<EntryKey, ClientBuildManifest> =
    new ManifestsMap()
  private fontManifests: ManifestsMap<EntryKey, NextFontManifest> =
    new ManifestsMap()
  private middlewareManifests: ManifestsMap<
    EntryKey,
    TurbopackMiddlewareManifest
  > = new ManifestsMap()
  private pagesManifests: ManifestsMap<string, PagesManifest> =
    new ManifestsMap()
  private sriManifests: ManifestsMap<EntryKey, SubresourceIntegrityManifest> =
    new ManifestsMap()
  private encryptionKey: string
  /// interceptionRewrites that have been written to disk
  /// This is used to avoid unnecessary writes if the rewrites haven't changed
  private cachedInterceptionRewrites: string | undefined = undefined
  private pendingCacheDeletes: string[] = []

  private readonly distDir: string
  private readonly buildId: string
  private readonly dev: boolean
  private readonly sriEnabled: boolean

  constructor({
    distDir,
    buildId,
    encryptionKey,
    dev,
    sriEnabled,
  }: {
    buildId: string
    distDir: string
    encryptionKey: string
    dev: boolean
    sriEnabled: boolean
  }) {
    this.distDir = distDir
    this.buildId = buildId
    this.encryptionKey = encryptionKey
    this.dev = dev
    this.sriEnabled = sriEnabled
  }

  delete(key: EntryKey) {
    this.actionManifests.delete(key)
    this.appPathsManifests.delete(key)
    this.buildManifests.delete(key)
    this.clientBuildManifests.delete(key)
    this.fontManifests.delete(key)
    this.middlewareManifests.delete(key)
    this.pagesManifests.delete(key)
  }

  loadActionManifest(pageName: string): void {
    this.actionManifests.set(
      getEntryKey('app', 'server', pageName),
      readPartialManifestContent(
        this.distDir,
        `${SERVER_REFERENCE_MANIFEST}.json`,
        pageName,
        'app'
      )
    )
  }

  private mergeActionManifests(manifests: Iterable<ActionManifest>) {
    type ActionEntries = ActionManifest['edge' | 'node']
    const manifest: ActionManifest = {
      node: {},
      edge: {},
      encryptionKey: this.encryptionKey,
    }

    function mergeActionIds(
      actionEntries: ActionEntries,
      other: ActionEntries
    ): void {
      for (const key in other) {
        const action = (actionEntries[key] ??= {
          workers: {},
        })
        action.filename = other[key].filename
        action.exportedName = other[key].exportedName
        Object.assign(action.workers, other[key].workers)
      }
    }

    for (const m of manifests) {
      mergeActionIds(manifest.node, m.node)
      mergeActionIds(manifest.edge, m.edge)
    }
    for (const key in manifest.node) {
      const entry = manifest.node[key]
      entry.workers = sortObjectByKey(entry.workers)
    }
    for (const key in manifest.edge) {
      const entry = manifest.edge[key]
      entry.workers = sortObjectByKey(entry.workers)
    }

    return manifest
  }

  private writeActionManifest(): void {
    if (!this.actionManifests.takeChanged()) {
      return
    }
    const actionManifest = this.mergeActionManifests(
      this.actionManifests.values()
    )
    const actionManifestJsonPath = join(
      this.distDir,
      'server',
      `${SERVER_REFERENCE_MANIFEST}.json`
    )
    const actionManifestJsPath = join(
      this.distDir,
      'server',
      `${SERVER_REFERENCE_MANIFEST}.js`
    )
    const json = JSON.stringify(actionManifest, null, 2)
    this.pendingCacheDeletes.push(actionManifestJsonPath)
    this.pendingCacheDeletes.push(actionManifestJsPath)
    writeFileAtomic(actionManifestJsonPath, json)
    writeFileAtomic(
      actionManifestJsPath,
      `self.__RSC_SERVER_MANIFEST=${JSON.stringify(json)}`
    )
  }

  loadAppPathsManifest(pageName: string): void {
    this.appPathsManifests.set(
      getEntryKey('app', 'server', pageName),
      readPartialManifestContent(
        this.distDir,
        APP_PATHS_MANIFEST,
        pageName,
        'app'
      )
    )
  }

  private writeAppPathsManifest(): void {
    if (!this.appPathsManifests.takeChanged()) {
      return
    }
    const appPathsManifest = this.mergePagesManifests(
      this.appPathsManifests.values()
    )
    const appPathsManifestPath = join(
      this.distDir,
      'server',
      APP_PATHS_MANIFEST
    )
    this.pendingCacheDeletes.push(appPathsManifestPath)
    writeFileAtomic(
      appPathsManifestPath,
      JSON.stringify(appPathsManifest, null, 2)
    )
  }

  private writeSriManifest(): void {
    if (!this.sriEnabled || !this.sriManifests.takeChanged()) {
      return
    }
    const sriManifest = this.mergeSriManifests(this.sriManifests.values())
    const pathJson = join(
      this.distDir,
      'server',
      `${SUBRESOURCE_INTEGRITY_MANIFEST}.json`
    )
    const pathJs = join(
      this.distDir,
      'server',
      `${SUBRESOURCE_INTEGRITY_MANIFEST}.js`
    )
    this.pendingCacheDeletes.push(pathJson)
    this.pendingCacheDeletes.push(pathJs)
    writeFileAtomic(pathJson, JSON.stringify(sriManifest, null, 2))
    writeFileAtomic(
      pathJs,
      `self.__SUBRESOURCE_INTEGRITY_MANIFEST=${JSON.stringify(
        JSON.stringify(sriManifest)
      )}`
    )
  }

  loadBuildManifest(pageName: string, type: 'app' | 'pages' = 'pages'): void {
    this.buildManifests.set(
      getEntryKey(type, 'server', pageName),
      readPartialManifestContent(this.distDir, BUILD_MANIFEST, pageName, type)
    )
  }

  loadClientBuildManifest(
    pageName: string,
    type: 'app' | 'pages' = 'pages'
  ): void {
    this.clientBuildManifests.set(
      getEntryKey(type, 'server', pageName),
      readPartialManifestContent(
        this.distDir,
        TURBOPACK_CLIENT_BUILD_MANIFEST,
        pageName,
        type
      )
    )
  }

  loadSriManifest(pageName: string, type: 'app' | 'pages' = 'pages'): void {
    if (!this.sriEnabled) return
    this.sriManifests.set(
      getEntryKey(type, 'client', pageName),
      readPartialManifestContent(
        this.distDir,
        `${SUBRESOURCE_INTEGRITY_MANIFEST}.json`,
        pageName,
        type
      )
    )
  }

  private mergeBuildManifests(
    manifests: Iterable<BuildManifest>,
    lowPriorityFiles: string[]
  ) {
    const manifest: Partial<BuildManifest> & Pick<BuildManifest, 'pages'> = {
      pages: {
        '/_app': [],
      },
      // Something in next.js depends on these to exist even for app dir rendering
      devFiles: [],
      polyfillFiles: [],
      lowPriorityFiles,
      rootMainFiles: [],
    }
    for (const m of manifests) {
      Object.assign(manifest.pages, m.pages)
      if (m.rootMainFiles.length) manifest.rootMainFiles = m.rootMainFiles
      // polyfillFiles should always be the same, so we can overwrite instead of actually merging
      if (m.polyfillFiles.length) manifest.polyfillFiles = m.polyfillFiles
    }
    manifest.pages = sortObjectByKey(manifest.pages) as BuildManifest['pages']
    return manifest
  }

  private mergeClientBuildManifests(
    manifests: Iterable<ClientBuildManifest>,
    rewrites: CustomRoutes['rewrites'],
    sortedPageKeys: string[]
  ): ClientBuildManifest {
    const manifest = {
      __rewrites: rewrites as any,
      sortedPages: sortedPageKeys,
    }
    for (const m of manifests) {
      Object.assign(manifest, m)
    }
    return sortObjectByKey(manifest)
  }

  private writeInterceptionRouteRewriteManifest(
    devRewrites: SetupOpts['fsChecker']['rewrites'] | undefined,
    productionRewrites: CustomRoutes['rewrites'] | undefined
  ): void {
    const rewrites = productionRewrites ?? {
      ...devRewrites,
      beforeFiles: (devRewrites?.beforeFiles ?? []).map(processRoute),
      afterFiles: (devRewrites?.afterFiles ?? []).map(processRoute),
      fallback: (devRewrites?.fallback ?? []).map(processRoute),
    }

    const interceptionRewrites = JSON.stringify(
      rewrites.beforeFiles.filter(
        (
          require('../../../lib/is-interception-route-rewrite') as typeof import('../../../lib/is-interception-route-rewrite')
        ).isInterceptionRouteRewrite
      )
    )

    if (this.cachedInterceptionRewrites === interceptionRewrites) {
      return
    }
    this.cachedInterceptionRewrites = interceptionRewrites

    const interceptionRewriteManifestPath = join(
      this.distDir,
      'server',
      `${INTERCEPTION_ROUTE_REWRITE_MANIFEST}.js`
    )
    this.pendingCacheDeletes.push(interceptionRewriteManifestPath)

    writeFileAtomic(
      interceptionRewriteManifestPath,
      `self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST=${JSON.stringify(
        interceptionRewrites
      )};`
    )
  }

  private writeBuildManifest(lowPriorityFiles: string[]): void {
    if (!this.buildManifests.takeChanged()) {
      return
    }
    const buildManifest = this.mergeBuildManifests(
      this.buildManifests.values(),
      lowPriorityFiles
    )

    const buildManifestPath = join(this.distDir, BUILD_MANIFEST)
    const middlewareBuildManifestPath = join(
      this.distDir,
      'server',
      `${MIDDLEWARE_BUILD_MANIFEST}.js`
    )

    this.pendingCacheDeletes.push(buildManifestPath)
    this.pendingCacheDeletes.push(middlewareBuildManifestPath)
    writeFileAtomic(buildManifestPath, JSON.stringify(buildManifest, null, 2))
    writeFileAtomic(
      middlewareBuildManifestPath,
      createEdgeRuntimeManifest(buildManifest)
    )

    // Write fallback build manifest
    const fallbackBuildManifest = this.mergeBuildManifests(
      [
        this.buildManifests.get(getEntryKey('pages', 'server', '_app')),
        this.buildManifests.get(getEntryKey('pages', 'server', '_error')),
      ].filter(Boolean) as BuildManifest[],
      lowPriorityFiles
    )
    const fallbackBuildManifestPath = join(
      this.distDir,
      `fallback-${BUILD_MANIFEST}`
    )
    this.pendingCacheDeletes.push(fallbackBuildManifestPath)
    writeFileAtomic(
      fallbackBuildManifestPath,
      JSON.stringify(fallbackBuildManifest, null, 2)
    )
  }

  private writeClientBuildManifest(
    entrypoints: Entrypoints,
    devRewrites: SetupOpts['fsChecker']['rewrites'] | undefined,
    productionRewrites: CustomRoutes['rewrites'] | undefined
  ): string[] {
    const rewrites = normalizeRewritesForBuildManifest(
      productionRewrites ?? {
        ...devRewrites,
        beforeFiles: (devRewrites?.beforeFiles ?? []).map(processRoute),
        afterFiles: (devRewrites?.afterFiles ?? []).map(processRoute),
        fallback: (devRewrites?.fallback ?? []).map(processRoute),
      }
    )

    const pagesKeys = [...entrypoints.page.keys()]
    if (entrypoints.global.app) {
      pagesKeys.push('/_app')
    }
    if (entrypoints.global.error) {
      pagesKeys.push('/_error')
    }

    const sortedPageKeys = getSortedRoutes(pagesKeys)

    let buildManifestPath = posix.join(
      CLIENT_STATIC_FILES_PATH,
      this.buildId,
      '_buildManifest.js'
    )
    let ssgManifestPath = posix.join(
      CLIENT_STATIC_FILES_PATH,
      this.buildId,
      '_ssgManifest.js'
    )

    if (
      this.dev &&
      !this.clientBuildManifests.takeChanged({ rewrites, sortedPageKeys })
    ) {
      return [buildManifestPath, ssgManifestPath]
    }

    const clientBuildManifest = this.mergeClientBuildManifests(
      this.clientBuildManifests.values(),
      rewrites,
      sortedPageKeys
    )
    const clientBuildManifestJs = `self.__BUILD_MANIFEST = ${JSON.stringify(
      clientBuildManifest,
      null,
      2
    )};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()`

    writeFileAtomic(
      join(this.distDir, buildManifestPath),
      clientBuildManifestJs
    )
    // This is just an empty placeholder, the actual manifest is written after prerendering in
    // packages/next/src/build/index.ts
    writeFileAtomic(join(this.distDir, ssgManifestPath), srcEmptySsgManifest)

    return [buildManifestPath, ssgManifestPath]
  }

  loadFontManifest(pageName: string, type: 'app' | 'pages' = 'pages'): void {
    this.fontManifests.set(
      getEntryKey(type, 'server', pageName),
      readPartialManifestContent(
        this.distDir,
        `${NEXT_FONT_MANIFEST}.json`,
        pageName,
        type
      )
    )
  }

  private mergeFontManifests(manifests: Iterable<NextFontManifest>) {
    const manifest: NextFontManifest = {
      app: {},
      appUsingSizeAdjust: false,
      pages: {},
      pagesUsingSizeAdjust: false,
    }
    for (const m of manifests) {
      Object.assign(manifest.app, m.app)
      Object.assign(manifest.pages, m.pages)

      manifest.appUsingSizeAdjust =
        manifest.appUsingSizeAdjust || m.appUsingSizeAdjust
      manifest.pagesUsingSizeAdjust =
        manifest.pagesUsingSizeAdjust || m.pagesUsingSizeAdjust
    }
    manifest.app = sortObjectByKey(manifest.app)
    manifest.pages = sortObjectByKey(manifest.pages)
    return manifest
  }

  private async writeNextFontManifest(): Promise<void> {
    if (!this.fontManifests.takeChanged()) {
      return
    }
    const fontManifest = this.mergeFontManifests(this.fontManifests.values())
    const json = JSON.stringify(fontManifest, null, 2)

    const fontManifestJsonPath = join(
      this.distDir,
      'server',
      `${NEXT_FONT_MANIFEST}.json`
    )
    const fontManifestJsPath = join(
      this.distDir,
      'server',
      `${NEXT_FONT_MANIFEST}.js`
    )
    this.pendingCacheDeletes.push(fontManifestJsonPath)
    this.pendingCacheDeletes.push(fontManifestJsPath)
    writeFileAtomic(fontManifestJsonPath, json)
    writeFileAtomic(
      fontManifestJsPath,
      `self.__NEXT_FONT_MANIFEST=${JSON.stringify(json)}`
    )
  }

  /**
   * @returns If the manifest was written or not
   */
  loadMiddlewareManifest(
    pageName: string,
    type: 'pages' | 'app' | 'middleware' | 'instrumentation'
  ): boolean {
    const middlewareManifestPath = getManifestPath(
      pageName,
      this.distDir,
      MIDDLEWARE_MANIFEST,
      type,
      true
    )

    // middlewareManifest is actually "edge manifest" and not all routes are edge runtime. If it is not written we skip it.
    if (!existsSync(middlewareManifestPath)) {
      return false
    }

    this.middlewareManifests.set(
      getEntryKey(
        type === 'middleware' || type === 'instrumentation' ? 'root' : type,
        'server',
        pageName
      ),
      readPartialManifestContent(
        this.distDir,
        MIDDLEWARE_MANIFEST,
        pageName,
        type
      )
    )

    return true
  }

  getMiddlewareManifest(key: EntryKey) {
    return this.middlewareManifests.get(key)
  }

  deleteMiddlewareManifest(key: EntryKey) {
    return this.middlewareManifests.delete(key)
  }

  private mergeMiddlewareManifests(
    manifests: Iterable<TurbopackMiddlewareManifest>
  ): MiddlewareManifest {
    const manifest: MiddlewareManifest = {
      version: 3,
      middleware: {},
      sortedMiddleware: [],
      functions: {},
    }
    let instrumentation: InstrumentationDefinition | undefined = undefined
    for (const m of manifests) {
      Object.assign(manifest.functions, m.functions)
      Object.assign(manifest.middleware, m.middleware)
      if (m.instrumentation) {
        instrumentation = m.instrumentation
      }
    }
    manifest.functions = sortObjectByKey(manifest.functions)
    manifest.middleware = sortObjectByKey(manifest.middleware)
    const updateFunctionDefinition = (
      fun: EdgeFunctionDefinition
    ): EdgeFunctionDefinition => {
      return {
        ...fun,
        files: [...(instrumentation?.files ?? []), ...fun.files],
      }
    }
    for (const key of Object.keys(manifest.middleware)) {
      const value = manifest.middleware[key]
      manifest.middleware[key] = updateFunctionDefinition(value)
    }
    for (const key of Object.keys(manifest.functions)) {
      const value = manifest.functions[key]
      manifest.functions[key] = updateFunctionDefinition(value)
    }
    for (const fun of Object.values(manifest.functions).concat(
      Object.values(manifest.middleware)
    )) {
      for (const matcher of fun.matchers) {
        if (!matcher.regexp) {
          matcher.regexp = safePathToRegexp(matcher.originalSource, [], {
            delimiter: '/',
            sensitive: false,
            strict: true,
          }).source.replaceAll('\\/', '/')
        }
      }
    }
    manifest.sortedMiddleware = Object.keys(manifest.middleware)

    return manifest
  }

  private writeMiddlewareManifest(): {
    clientMiddlewareManifestPath: string
  } {
    let clientMiddlewareManifestPath = posix.join(
      CLIENT_STATIC_FILES_PATH,
      this.buildId,
      TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST
    )

    if (this.dev && !this.middlewareManifests.takeChanged()) {
      return {
        clientMiddlewareManifestPath,
      }
    }
    const middlewareManifest = this.mergeMiddlewareManifests(
      this.middlewareManifests.values()
    )

    // Server middleware manifest

    // Normalize regexes as it uses path-to-regexp
    for (const key in middlewareManifest.middleware) {
      middlewareManifest.middleware[key].matchers.forEach((matcher) => {
        if (!matcher.regexp.startsWith('^')) {
          const parsedPage = tryToParsePath(matcher.regexp)
          if (parsedPage.error || !parsedPage.regexStr) {
            throw new Error(`Invalid source: ${matcher.regexp}`)
          }
          matcher.regexp = parsedPage.regexStr
        }
      })
    }

    const middlewareManifestPath = join(
      this.distDir,
      'server',
      MIDDLEWARE_MANIFEST
    )
    this.pendingCacheDeletes.push(middlewareManifestPath)
    writeFileAtomic(
      middlewareManifestPath,
      JSON.stringify(middlewareManifest, null, 2)
    )

    // Client middleware manifest This is only used in dev though, packages/next/src/build/index.ts
    // writes the mainfest again for builds.
    const matchers = middlewareManifest?.middleware['/']?.matchers || []

    const clientMiddlewareManifestJs = `self.__MIDDLEWARE_MATCHERS = ${JSON.stringify(
      matchers,
      null,
      2
    )};self.__MIDDLEWARE_MATCHERS_CB && self.__MIDDLEWARE_MATCHERS_CB()`

    this.pendingCacheDeletes.push(clientMiddlewareManifestPath)
    writeFileAtomic(
      join(this.distDir, clientMiddlewareManifestPath),
      clientMiddlewareManifestJs
    )

    return {
      clientMiddlewareManifestPath,
    }
  }

  loadPagesManifest(pageName: string): void {
    this.pagesManifests.set(
      getEntryKey('pages', 'server', pageName),
      readPartialManifestContent(this.distDir, PAGES_MANIFEST, pageName)
    )
  }

  private mergePagesManifests(manifests: Iterable<PagesManifest>) {
    const manifest: PagesManifest = {}
    for (const m of manifests) {
      Object.assign(manifest, m)
    }
    return sortObjectByKey(manifest)
  }

  private mergeSriManifests(manifests: Iterable<SubresourceIntegrityManifest>) {
    const manifest: SubresourceIntegrityManifest = {}
    for (const m of manifests) {
      Object.assign(manifest, m)
    }
    return sortObjectByKey(manifest)
  }

  private writePagesManifest(): void {
    if (!this.pagesManifests.takeChanged()) {
      return
    }
    const pagesManifest = this.mergePagesManifests(this.pagesManifests.values())
    const pagesManifestPath = join(this.distDir, 'server', PAGES_MANIFEST)
    this.pendingCacheDeletes.push(pagesManifestPath)
    writeFileAtomic(pagesManifestPath, JSON.stringify(pagesManifest, null, 2))
  }

  writeManifests({
    devRewrites,
    productionRewrites,
    entrypoints,
  }: {
    devRewrites: SetupOpts['fsChecker']['rewrites'] | undefined
    productionRewrites: CustomRoutes['rewrites'] | undefined
    entrypoints: Entrypoints
  }): void {
    this.writeActionManifest()
    this.writeAppPathsManifest()
    const lowPriorityFiles = this.writeClientBuildManifest(
      entrypoints,
      devRewrites,
      productionRewrites
    )
    const { clientMiddlewareManifestPath } = this.writeMiddlewareManifest()
    this.writeBuildManifest([...lowPriorityFiles, clientMiddlewareManifestPath])
    this.writeInterceptionRouteRewriteManifest(devRewrites, productionRewrites)
    this.writeNextFontManifest()
    this.writePagesManifest()

    this.writeSriManifest()

    // Flush all queued cache deletions in a single require.cache scan
    if (this.pendingCacheDeletes.length > 0) {
      deleteCache(this.pendingCacheDeletes)
      this.pendingCacheDeletes = []
    }
  }
}

function sortObjectByKey(obj: Record<string, any>) {
  return Object.keys(obj)
    .sort()
    .reduce(
      (acc, key) => {
        acc[key] = obj[key]
        return acc
      },
      {} as Record<string, any>
    )
}
Quest for Codev2.0.0
/
SIGN IN