Skip to main content
doc0blade47/next.jsdocumentation home
Sign in

Manifest Plugins

Last updated Oct 12, 2026
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Manifest Plugins form the backbone of Next.js asset management and runtime dependency resolution by intercepting compiler compilation hooks to generate, serialize, and persist critical JSON and JavaScript manifest files. These plugins bridge static bundling output and dynamic server or client execution, enabling featuresBuild Pipeline such as server component client mapping, font preloading tables, dynamic import chunk tracking, and edge runtime route bindings across Webpack and Turbopack Engine compilation pipelines. Sources: packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts:206-216, packages/next/src/build/webpack/plugins/next-font-manifest-plugin.ts:55-61, packages/next/src/build/webpack/plugins/build-manifest-plugin.ts:102-128

Build Manifest Compilation and Asset Generation

Initialization and Compilation Hook Registration

BuildManifestPlugin orchestrates asset collection and serialization by tapping into Webpack compilation lifecycles. During initialization, the constructor accepts configuration options—buildId, rewrites, isDevFallback, appDirEnabled, and clientRouterFilters—and normalizes custom route rewrites via processRoute across beforeFiles, afterFiles, and fallback arrays.

export default class BuildManifestPlugin {
  private buildId: string
  private rewrites: CustomRoutes['rewrites']
  private isDevFallback: boolean
  private appDirEnabled: boolean
  private clientRouterFilters?: Parameters<typeof generateClientManifest>[2]
// ...

Sources: packages/next/src/build/webpack/plugins/build-manifest-plugin.ts:102-128

The plugin registers its compilation lifecycle within the compiler's make hook, tapping into processAssets at stage webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS to execute createAssets.

  apply(compiler: webpack.Compiler) {
    compiler.hooks.make.tap('NextJsBuildManifest', (compilation: any) => {
      compilation.hooks.processAssets.tap(
        {
          name: 'NextJsBuildManifest',
          stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
        },
        () => {
          this.createAssets(compiler, compilation)
        }
      )
    })
    return
  }

Sources: packages/next/src/build/webpack/plugins/build-manifest-plugin.ts:266-279


Asset Collection and Entrypoint Processing

Within createAssets, the plugin initializes an empty asset map tracking polyfills, development files, low-priority bundles, root main files, and page-specific dependency arrays. It filters out .hot-update.js extensions from entrypoint files via getEntrypointFiles and standardizes path separators to forward slashes.

export function getEntrypointFiles(entrypoint: any): string[] {
  return (
    entrypoint
      ?.getFiles()
      .filter((file: string) => {
        // We don't want to include `.hot-update.js` files into the initial page
        return /(?<!\.hot-update)\.(js|css)($|\?)/.test(file)
      })
      .map((file: string) => file.replace(/\\/g, '/')) ?? []
  )
}

Sources: packages/next/src/build/webpack/plugins/build-manifest-plugin.ts:88-98

Polyfill files are extracted by iterating over compilation assets where .js files carry the CLIENT_STATIC_FILES_RUNTIME_POLYFILLS_SYMBOL info property. React refresh development dependencies are isolated by filtering entrypoints against the main runtime chunk. Non-system entrypoints are mapped to their respective route paths using getRouteFromEntrypoint.

Note

_app dependencies are collected separately as the baseline set. When generating the reduced client build manifest via generateClientManifest, dependencies already present in /_app are stripped from individual page asset arrays to prevent redundant loading during client-side navigations.

export function generateClientManifest(
  assetMap: BuildManifest,
  rewrites: CustomRoutes['rewrites'],
  clientRouterFilters?: {
    staticFilter: ReturnType<BloomFilter['export']>
    dynamicFilter: ReturnType<BloomFilter['export']>
  },
  compiler?: any,
  compilation?: any
): string | undefined {
// ...

Sources: packages/next/src/build/webpack/plugins/build-manifest-plugin.ts:36-45


Serialization and Asset Emission

Once the asset map is compiled and sorted alphabetically by route key, the plugin emits multiple manifest artifacts into the Webpack compilation output using sources.RawSource.

Manifest Asset TargetEmitter Path / NameSerialization Format / Utility
Main Build ManifestbuildManifestName (build-manifest.json or fallback-build-manifest.json)JSON.stringify(assetMap, null, 2)
Middleware Edge Manifestserver/${MIDDLEWARE_BUILD_MANIFEST}.jscreateEdgeRuntimeManifest(assetMap)
Client Build Manifeststatic/${buildId}/_buildManifest.jsself.__BUILD_MANIFEST = ${generateClientManifest(...)}
SSG Manifeststatic/${buildId}/_ssgManifest.jssrcEmptySsgManifest

Sources: packages/next/src/build/webpack/plugins/build-manifest-plugin.ts:229-261, packages/next/src/build/webpack/plugins/build-manifest-plugin-utils.ts:11-58

Caution

External rewrite destinations lacking a leading slash are actively pruned from destination properties during route processing so that client-side routers do not attempt invalid relative resolutions. Sources: packages/next/src/build/webpack/plugins/build-manifest-plugin-utils.ts:25-34

export const processRoute = (r: Rewrite) => {
  const rewrite = { ...r }
 
  // omit external rewrite destinations since these aren't
  // handled client-side
  if (!rewrite?.destination?.startsWith('/')) {
    delete (rewrite as any).destination
  }
  return rewrite
}

Sources: packages/next/src/build/webpack/plugins/build-manifest-plugin-utils.ts:25-34

Flight Manifest and Client Reference Resolution

Flight Manifest Plugin State and Structures

const pluginState = getProxiedPluginState({
  ssrModules: {} as { [ssrModuleId: string]: ModuleInfo },
  edgeSsrModules: {} as { [ssrModuleId: string]: ModuleInfo },
 
  rscModules: {} as { [rscModuleId: string]: ModuleInfo },
  edgeRscModules: {} as { [rscModuleId: string]: ModuleInfo },
})

Sources: packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts:46-52

const pluginState = getProxiedPluginState({
  // A map to track "action" -> "list of bundles".
  serverActions: {} as ActionManifest['node'],
  edgeServerActions: {} as ActionManifest['edge'],
 
  serverActionModules: {} as {
    [workerName: string]: {
      server?: ModuleInfo
      client?: ModuleInfo
    }
  },
 
  edgeServerActionModules: {} as {
    [workerName: string]: {
      server?: ModuleInfo
      client?: ModuleInfo
    }
  },
 
  ssrModules: {} as { [ssrModuleId: string]: ModuleInfo },
  edgeSsrModules: {} as { [ssrModuleId: string]: ModuleInfo },
 
  rscModules: {} as { [rscModuleId: string]: ModuleInfo },
  edgeRscModules: {} as { [rscModuleId: string]: ModuleInfo },
 
  injectedClientEntries: {} as Record<string, string>,
})

Sources: packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts:115-141

The client reference resolution pipeline uses proxied plugin states to aggregate module information across compilation targets. ClientReferenceManifestPlugin builds mapping structures that connect Server Components, Server-Side Rendered (SSR) modules, and Edge runtime modules to their corresponding Webpack chunk groups and file lists.

export interface ManifestNode {
  [moduleExport: string]: {
    id: ModuleId
    name: string
    chunks: ManifestChunks
    async?: boolean
  }
}
 
export interface ClientReferenceManifestForRsc {
  clientModules: ManifestNode
  rscModuleMapping: {
    [moduleId: string]: ManifestNode
  }
  edgeRscModuleMapping: {
    [moduleId: string]: ManifestNode
  }
}

Sources: packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts:54-84

Note

Client reference chunks are encoded as URIs via encodeURIPath and augmented with asset token queries so that React's Flight runtime manages filenames and reserved characters (such as [ and ]) correctly for servers like S3. Sources: packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts:141-147

Chunk Grouping and Entry Normalization

Entry names are normalized to group names via entryNameToGroupName to ensure that parallel routes, loading UI, and page components sharing a route segment merge their manifests correctly.

function entryNameToGroupName(entryName: string) {
  let groupName = entryName
    .slice(0, entryName.lastIndexOf('/'))
    // Remove slots
    .replace(/\/@[^/]+/g, '')
    // Remove the group with lookahead to make sure it's not interception route
    .replace(/\/\([^/]+\)(?=(\/|$))/g, '')
    // Remove catch-all routes since they should be part of the parent group that the catch-all would apply to.
    // This is necessary to support parallel routes since multiple page components can be rendered on the same page.
    // In order to do that, we need to ensure that the manifests are merged together by putting them in the same group.
    .replace(/\/\[?\[\.\.\.[^\]]*]]?/g, '')
 
  // Interception routes
  groupName = groupName
    .replace(/^.+\/\(\.\.\.\)/g, 'app/')
    .replace(/\/\(\.\)/g, '/')
 
  // Interception routes (recursive)
  while (/\/[^/]+\/\(\.\.\)/.test(groupName)) {
    groupName = groupName.replace(/\/[^/]+\/\(\.\.\)/g, '/')
  }
 
  return groupName
}

Sources: packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts:161-184

When extracting required chunks from chunk groups via getAppPathRequiredChunks, non-JavaScript files, hot updates, system entrypoints, and files present in the root main bundle (excludedFiles) are omitted.

Parameter / HelperTarget / BehaviorPurpose
chunkGroup.chunkswebpack.ChunkGroupIterates through assigned chunks to extract module IDs and asset files
SYSTEM_ENTRYPOINTSSet<string>Skips internal system chunks from being duplicated in route-level manifests
excludedFilesSet<string>Omits root main files already loaded globally by the main application entrypoint
encodeURIPathstringEncodes path components for compatibility with remote asset servers

Sources: packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts:118-150

Warning

Inlined CSS files are only bundled directly into the manifest when experimentalInlineCss is enabled and dev mode is false, because inline CSS does not currently function properly with Hot Module Replacement (HMR). Sources: packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts:299-315

Pages Manifest Mapping and Disk Persistence

Pages Manifest Plugin Mechanics

PagesManifestPlugin implements webpack.WebpackPluginInstance to construct route mapping files such as pages-manifest.json and app-paths-manifest.json from Webpack compilation entrypoints. During the make compilation hook, it registers a processAssets hook at stage webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS to execute createAssets.

apply(compiler: webpack.Compiler): void {
  compiler.hooks.make.tap('NextJsPagesManifest', (compilation) => {
    compilation.hooks.processAssets.tapPromise(
      {
        name: 'NextJsPagesManifest',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
      },
      () => this.createAssets(compilation)
    )
  })
}

Sources: packages/next/src/build/webpack/plugins/pages-manifest-plugin.ts:181-191

The plugin iterates over compilation entrypoints, mapping each entry name via getRouteFromEntrypoint. It filters out files containing webpack-runtime or webpack-api-runtime, and retains only those ending in .js.

async createAssets(compilation: any) {
  const entrypoints = compilation.entrypoints
  const pages: PagesManifest = {}
  const appPaths: PagesManifest = {}
 
  for (const entrypoint of entrypoints.values()) {
    const pagePath = getRouteFromEntrypoint(
      entrypoint.name,
      this.appDirEnabled
    )
 
    if (!pagePath) {
      continue
    }
 
    const files = entrypoint
      .getFiles()
      .filter(
        (file: string) =>
          !file.includes('webpack-runtime') &&
          !file.includes('webpack-api-runtime') &&
          file.endsWith('.js')
      )
 
    // Skip entries which are empty
    if (!files.length) {
      continue
    }

Sources: packages/next/src/build/webpack/plugins/pages-manifest-plugin.ts:46-73

Property / OptionTypePurpose
devbooleanIndicates whether compilation runs in development mode
distDirstring | undefinedTarget distribution directory for writing persistent disk assets
isEdgeRuntimebooleanSpecifies if compilation targets the Edge runtime versus Node.js
appDirEnabledbooleanControls whether the App Directory (app/) route mapping is active

Sources: packages/next/src/build/webpack/plugins/pages-manifest-plugin.ts:24-44

Note

In production non-Edge builds, the initial three characters (.js) are sliced off production entry filenames before normalization, whereas development mode and Edge runtime builds retain the full file path. Sources: packages/next/src/build/webpack/plugins/pages-manifest-plugin.ts:77-83

Multi-Compiler Manifest Merging and Disk Persistence

Because Next.js utilizes parallel compilers for the Node server and Edge server, PagesManifestPlugin stores module entries in module-level global variables (edgeServerPages, nodeServerPages, edgeServerAppPaths, nodeServerAppPaths) and merges them upon asset creation.

export let edgeServerPages = {}
export let nodeServerPages = {}
export let edgeServerAppPaths = {}
export let nodeServerAppPaths = {}

Sources: packages/next/src/build/webpack/plugins/pages-manifest-plugin.ts:13-16

When distDir is specified, writeMergedManifest ensures parent directories via fs.mkdir with recursive options, reads any existing manifest JSON from disk, merges existing entries with new entries, and writes the JSON payload back out with an indentation of 2 spaces. If distDir is absent, assets are emitted directly to Webpack compilation via compilation.emitAsset using sources.RawSource.

// handle parallel compilers writing to the same
// manifest path by merging existing manifest with new
const writeMergedManifest = async (
  manifestPath: string,
  entries: Record<string, string>
) => {
  await fs.mkdir(path.dirname(manifestPath), { recursive: true })
  await fs.writeFile(
    manifestPath,
    JSON.stringify(
      {
        ...(await fs
          .readFile(manifestPath, 'utf8')
          .then((res) => JSON.parse(res))
          .catch(() => ({}))),
        ...entries,
      },
      null,
      2
    )
  )
}

Sources: packages/next/src/build/webpack/plugins/pages-manifest-plugin.ts:101-122

Warning

Parallel compiler instances write to the same target manifest paths on disk, making the file read-merge-write cycle inside writeMergedManifest necessary to prevent race conditions from overwriting entries generated by separate compiler targets. Sources: packages/next/src/build/webpack/plugins/pages-manifest-plugin.ts:101-122

Font Manifest Optimization and Preloading Tables

Module Traversal and Font Asset Filtering

The NextFontManifestPlugin implements webpack.WebpackPluginInstance to track font assets generated by next-font-loader, producing optimized font preloading tables and size-adjust metadata for both the pages/ and app/ directory architectures. During the make compilation hook, it hooks into processAssets at stage webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS to inspect module assets and chunk entrypoints.

export class NextFontManifestPlugin {
  private appDir: undefined | string
 
  constructor(options: { appDir: undefined | string }) {
    this.appDir = options.appDir
  }
 
  apply(compiler: webpack.Compiler) {
    compiler.hooks.make.tap(PLUGIN_NAME, (compilation) => {
      // In this stage the font files are emitted and we can collect all files emitted by each chunkGroup (entry).
      compilation.hooks.processAssets.tap(
        {
          name: PLUGIN_NAME,
          stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
        },
        () => {
          const nextFontManifest: NextFontManifest = {
            pages: {},
            app: {},
            appUsingSizeAdjust: false,
            pagesUsingSizeAdjust: false,
          }

Sources: packages/next/src/build/webpack/plugins/next-font-manifest-plugin.ts:55-76

For app/ directory builds, the plugin uses traverseModules to iterate over modules created within chunk groups whose names start with app/. It checks each module request for /next-font-loader/index.js? and extracts emitted font files ending with .woff, .woff2, .eot, .ttf, or .otf from mod.buildInfo.assets. For pages/ directory entries, it loops through compilation entrypoints, resolves the route path via getRouteFromEntrypoint, and inspects chunk.auxiliaryFiles across all chunks assigned to the entrypoint.

if (this.appDir) {
  const appDirBase = path.dirname(this.appDir) + path.sep
 
  // After all modules are created, we collect the modules that was created by next-font-loader.
  traverseModules(
    compilation,
    (mod, _chunk, chunkGroup) => {
      if (mod?.request?.includes('/next-font-loader/index.js?')) {
        if (!mod.buildInfo?.assets) return
 
        const chunkEntryName = (appDirBase + chunkGroup.name).replace(
          /[\\/]/g,
          path.sep
        )
 
        const modAssets = Object.keys(mod.buildInfo.assets)
        const fontFiles: string[] = modAssets.filter((file: string) =>
          /\.(woff|woff2|eot|ttf|otf)$/.test(file)
        )
 
        // Look if size-adjust fallback font is being used
        if (!nextFontManifest.appUsingSizeAdjust) {
          nextFontManifest.appUsingSizeAdjust =
            getPageIsUsingSizeAdjust(fontFiles)
        }
 
        const preloadedFontFiles = getPreloadedFontFiles(fontFiles)
 
        // Add an entry of the module's font files in the manifest.
        // We'll add an entry even if no files should preload.
        // When an entry is present but empty, instead of preloading the font files, a preconnect tag is added.
        if (fontFiles.length > 0) {
          if (!nextFontManifest.app[chunkEntryName]) {
            nextFontManifest.app[chunkEntryName] = []
          }
          nextFontManifest.app[chunkEntryName].push(
            ...preloadedFontFiles
          )
        }
      }
    },
    (chunkGroup) => {
      // Only loop through entrypoints that are under app/.
      return !!chunkGroup.name?.startsWith('app/')
    }
  )
}
 
// Look at all the entrypoints created for pages/.
for (const entrypoint of compilation.entrypoints.values()) {
  const pagePath = getRouteFromEntrypoint(entrypoint.name!)
 
  if (!pagePath) {
    continue
  }
 
  // Get font files from the chunks included in the entrypoint.
  const fontFiles: string[] = entrypoint.chunks
    .flatMap((chunk: any) => [...chunk.auxiliaryFiles])
    .filter((file: string) =>
      /\.(woff|woff2|eot|ttf|otf)$/.test(file)
    )
 
  // Look if size-adjust fallback font is being used
  if (!nextFontManifest.pagesUsingSizeAdjust) {
    nextFontManifest.pagesUsingSizeAdjust =
      getPageIsUsingSizeAdjust(fontFiles)
  }
 
  const preloadedFontFiles = getPreloadedFontFiles(fontFiles)
 
  // Add an entry of the route's font files in the manifest.
  // We'll add an entry even if no files should preload.
  // When an entry is present but empty, instead of preloading the font files, a preconnect tag is added.
  if (fontFiles.length > 0) {
    nextFontManifest.pages[pagePath] = preloadedFontFiles
  }
}

Sources: packages/next/src/build/webpack/plugins/next-font-manifest-plugin.ts:78-155

Helper FunctionTarget Extension / PatternPurpose
getPreloadedFontFiles/\.p\.(woff|woff2|eot|ttf|otf)$/Filters font files to return only those designated for preloading (.p.)
getPageIsUsingSizeAdjustfile.includes('-s')Detects whether fallback fonts incorporate size-adjust flags for layout shift metrics

Sources: packages/next/src/build/webpack/plugins/next-font-manifest-plugin.ts:26-39

Note

When a module or entrypoint contains font files but none are marked with the preloading flag (.p.), the manifest retains an empty array for that route or module key. During rendering in _document.tsx or app-render, an empty array triggers a preconnect tag instead of a preload link tag for the font family. Sources: packages/next/src/build/webpack/plugins/next-font-manifest-plugin.ts:107-116

Manifest Serialization and Asset Emission

The resulting NextFontManifest structure organizes preloading configuration and size-adjust booleans into separate maps for pages and app routes. The plugin serializes this manifest into two separate compilation assets using sources.RawSource: a JSON file written to server/${NEXT_FONT_MANIFEST}.json for Node server consumption, and a JavaScript assignment wrapping self.__NEXT_FONT_MANIFEST written to server/${NEXT_FONT_MANIFEST}.js for the Edge Runtime.

export type NextFontManifest = {
  pages: {
    [path: string]: string[]
  }
  app: {
    [entry: string]: string[]
  }
  appUsingSizeAdjust: boolean
  pagesUsingSizeAdjust: boolean
}

Sources: packages/next/src/build/webpack/plugins/next-font-manifest-plugin.ts:7-16

  const manifest = JSON.stringify(nextFontManifest, null)
  // Create manifest for edge
  compilation.emitAsset(
    `server/${NEXT_FONT_MANIFEST}.js`,
    new sources.RawSource(
      `self.__NEXT_FONT_MANIFEST=${JSON.stringify(manifest)}`
    ) as unknown as webpack.sources.RawSource
  )
 
  // Create manifest for server
  compilation.emitAsset(
    `server/${NEXT_FONT_MANIFEST}.json`,
    new sources.RawSource(
      manifest
    ) as unknown as webpack.sources.RawSource
  )
}

Sources: packages/next/src/build/webpack/plugins/next-font-manifest-plugin.ts:157-173

Warning

The app manifest map stores entries keyed by normalized webpack module chunk entry paths (appDirBase + chunkGroup.name), whereas the pages manifest map uses resolved route paths (pagePath), reflecting the differing module-level versus entrypoint-level resolution strategies required by the two routing models. Sources: packages/next/src/build/webpack/plugins/next-font-manifest-plugin.ts:50-54

Loadable Components and Dynamic Import Manifests

Tracking Dynamic Imports and Webpack Chunk Blocks

The ReactLoadablePlugin extracts dynamic import associations during Webpack compilation to build the React Loadable manifest. Hooking into compilation.hooks.processAssets at webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, the plugin executes buildManifest to traverse compilation modules and dependency blocks.

apply(compiler: webpack.Compiler) {
  compiler.hooks.make.tap('ReactLoadableManifest', (compilation) => {
    compilation.hooks.processAssets.tap(
      {
        name: 'ReactLoadableManifest',
        stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
      },
      () => {
        this.createAssets(compiler, compilation)
      }
    )
  })
}

Sources: packages/next/src/build/webpack/plugins/react-loadable-plugin.ts:239-251

It inspects dependency blocks recursively via handleBlock, matching dependencies whose types start with import(). Standard eager imports or context dependencies (import(/* webpackMode: "eager" */ ...) or import(./module/${param})) are unsupported and skipped.

// Find all dependencies blocks which contains a `import()` dependency
const handleBlock = (block: any) => {
  block.blocks.forEach(handleBlock)
  const chunkGroup = getChunkGroupFromBlock(compilation, block)
  for (const dependency of block.dependencies) {
    if (dependency.type.startsWith('import()')) {

Sources: packages/next/src/build/webpack/plugins/react-loadable-plugin.ts:83-88

Caution

The plugin explicitly ignores ImportEagerDependency and ImportContextDependency types. Dynamic expressions with template literals or eager webpack modes will fail to register in the loadable manifest, breaking server-side chunk preloading for those components. Sources: packages/next/src/build/webpack/plugins/react-loadable-plugin.ts:79-82

Manifest Compilation and Asset Generation

For every matched import() dependency, the plugin derives an origin module request from compilation.moduleGraph.getParentModule(dependency) and constructs a unique manifest key using the relative path from projectSrcDir combined with the dependency request string.

// get the module containing the import()
const originModule = getOriginModuleFromDependency(
  compilation,
  dependency
)
const originRequest: string | undefined = originModule?.resource
if (!originRequest) return
 
// We construct a "unique" key from origin module and request
// It's not perfect unique, but that will be fine for us.
// We also need to construct the same in the babel plugin.
const key = `${path.relative(projectSrcDir, originRequest)} -> ${
  dependency.request
}`

Sources: packages/next/src/build/webpack/plugins/react-loadable-plugin.ts:93-106

It then resolves chunk groups associated with the block via compilation.chunkGraph.getBlockChunkGroup(block) and gathers all generated .js and .css files residing under static/chunks/ or static/css/.

if (chunkGroup) {
  for (const chunk of (chunkGroup as any)
    .chunks as webpack.Compilation['chunks']) {
    chunk.files.forEach((file: string) => {
      if (
        (file.endsWith('.js') || file.endsWith('.css')) &&
        file.match(/^static\/(chunks|css)\//)
      ) {
        files.add(file)
 
        if (shouldCreateDynamicCssManifest && file.endsWith('.css')) {
          dynamicCssManifestSet.add(file)
        }
      }
    })
  }
}

Sources: packages/next/src/build/webpack/plugins/react-loadable-plugin.ts:125-141

Configuration Property / AssetTarget DestinationPurpose
reactLoadableManifestreact-loadable-manifest.jsonMaps dynamic import keys to Webpack module IDs and assigned chunk files
runtimeAssetServer runtime bundleInjects self.__REACT_LOADABLE_MANIFEST for Edge Runtime execution
dynamicCssManifestdynamic-css-manifest.jsonTracks dynamic CSS assets to prevent removing server-rendered <link> tags after client navigation
Edge CSS Assetserver/dynamic-css-manifest.jsAssigns self.__DYNAMIC_CSS_MANIFEST for Edge Runtime stylesheet persistence

Sources: packages/next/src/build/webpack/plugins/react-loadable-plugin.ts:202-236

Note

In development mode (this.dev), the manifest uses the string key (key) as the module identifier instead of the numeric Webpack module ID (compilation.chunkGraph.getModuleId(module)), ensuring stable identifiers across hot module reloading iterations. Sources: packages/next/src/build/webpack/plugins/react-loadable-plugin.ts:147-149

Middleware Manifest and Edge Function Assets

Route Matching Regex Generation and Asset Binding

The middleware compilation pipeline coordinates route regex generation and metadata binding for middleware and Edge functions through MiddlewarePlugin. During asset creation, the plugin iterates over all Webpack entrypoints to locate metadata associated with edgeMiddleware, edgeSSR, or edgeApiFunction. For each matched entrypoint, it normalizes App Router paths or page paths and invokes getNamedMiddlewareRegex to build precise route matching expressions.

const matcherSource = metadata.edgeSSR?.isAppDir
  ? normalizeAppPath(page)
  : page
 
const catchAll = !metadata.edgeSSR && !metadata.edgeApiFunction
 
const { namedRegex } = getNamedMiddlewareRegex(matcherSource, {
  catchAll,
})
const matchers = metadata?.edgeMiddleware?.matchers ?? [
  {
    regexp: namedRegex,
    originalSource: page === '/' && catchAll ? '/:path*' : matcherSource,
  },
]

Sources: packages/next/src/build/webpack/plugins/middleware-plugin.ts:232-246

Edge function definitions compile asset bindings and WASM modules directly from entry metadata, mapping binding maps into explicit arrays containing name and file path properties for runtime consumption.

wasm: Array.from(metadata.wasmBindings, ([name, filePath]) => ({
  name,
  filePath,
})),
assets: Array.from(metadata.assetBindings, ([name, filePath]) => ({
  name,
  filePath,
})),

Sources: packages/next/src/build/webpack/plugins/middleware-plugin.ts:260-267

Manifest Field / TypeProperty / ConstantDescription
MiddlewareManifestversion: 3Specifies version 3 for the serialized middleware and edge functions descriptor
EdgeFunctionDefinitionfiles: string[]List of compiled output files associated with the Edge function entrypoint
EdgeFunctionDefinitionmatchers: ProxyMatcher[]Array of route matching objects containing compiled regular expressions and original sources
EdgeFunctionDefinitionwasm: AssetBinding[]WebAssembly modules bound to the Edge function execution context
EdgeFunctionDefinitionassets: AssetBinding[]Static assets bound to the Edge function
EdgeFunctionDefinitionenv: Record<string, string>Environment variables injected into the Edge runtime environment

Sources: packages/next/src/build/webpack/plugins/middleware-plugin.ts:44-64, packages/next/src/build/webpack/plugins/middleware-plugin.ts:192-197

Note

Interception route rewrites are filtered from custom configuration and emitted as a standalone self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST assignment in interception-route-rewrite-manifest.js to supply the Edge runtime with necessary routing context without parsing the entire routes manifest. Sources: packages/next/src/build/webpack/plugins/middleware-plugin.ts:203-215

Static Analysis Verification and Unsupported APIs

The plugin runs code analysis during JavaScript parser hooks to inspect modules residing within the middleware or edge API Webpack layers (WEBPACK_LAYERS.middleware or WEBPACK_LAYERS.apiEdge). It checks calls and member expressions against EDGE_UNSUPPORTED_NODE_APIS, generating compilation warnings when unsupported Node.js built-ins or process methods are invoked.

function registerUnsupportedApiHooks(
  parser: webpack.javascript.JavascriptParser,
  compilation: webpack.Compilation
) {
  for (const expression of EDGE_UNSUPPORTED_NODE_APIS) {
    const warnForUnsupportedApi = (node: any) => {
      if (!isInMiddlewareLayer(parser)) {
        return
      }
      compilation.warnings.push(
        buildUnsupportedApiError({
          compilation,
          parser,
          apiName: expression,
          ...node,
        })
      )
      return true
    }
    parser.hooks.call.for(expression).tap(NAME, warnForUnsupportedApi)
    parser.hooks.expression.for(expression).tap(NAME, warnForUnsupportedApi)
    parser.hooks.callMemberChain
      .for(expression)
      .tap(NAME, warnForUnsupportedApi)
    parser.hooks.expressionMemberChain
      .for(expression)
      .tap(NAME, warnForUnsupportedApi)
  }

Sources: packages/next/src/build/webpack/plugins/middleware-plugin.ts:370-397

Dynamic code evaluation is validated against known safe edge packages and picomatch rules defined in unstable_allowDynamic configurations to decide whether packages utilizing eval are permissible within the Edge Runtime.

function isDynamicCodeEvaluationAllowed(
  fileName: string,
  middlewareConfig?: ProxyConfig,
  rootDir?: string
) {
  // Some packages are known to use `eval` but are safe to use in the Edge
  // Runtime because the dynamic code will never be executed.
  if (
    KNOWN_SAFE_DYNAMIC_PACKAGES.some((pkg) =>
      fileName.includes(`/node_modules/${pkg}/`.replace(/\//g, path.sep))
    )
  ) {
    return true
  }
 
  const name = fileName.replace(rootDir ?? '', '')
 
  return picomatch(middlewareConfig?.unstable_allowDynamic ?? [], {
    dot: true,
  })(name)
}

Sources: packages/next/src/build/webpack/plugins/middleware-plugin.ts:330-350

Warning

Accessing process properties other than process.env inside the middleware layer triggers a warning via registerUnsupportedApiHooks, because most process properties are unavailable in the lightweight Edge Runtime. Sources: packages/next/src/build/webpack/plugins/middleware-plugin.ts:399-413

Was this page helpful?