next.js/packages/next/src/build/webpack/plugins/deferred-entries-plugin.ts
deferred-entries-plugin.ts141 lines4.2 KB
import { webpack } from 'next/dist/compiled/webpack/webpack'
import type { NextConfigComplete } from '../../../server/config-shared'
import createDebug from 'next/dist/compiled/debug'

const debug = createDebug('next:deferred-entries-plugin')
const PLUGIN_NAME = 'DeferredEntriesPlugin'

interface DeferredEntriesPluginOptions {
  dev: boolean
  config: NextConfigComplete
  deferredEntrypoints?: webpack.EntryObject
}

/**
 * A webpack plugin that handles deferred entries by:
 * 1. Accepting deferred entrypoints separately from the main config
 * 2. After non-deferred entries are compiled, calling the onBeforeDeferredEntries callback
 * 3. Then adding and compiling the deferred entries within the same compilation
 *
 * This approach avoids module ID conflicts that would occur with separate compilations.
 */
export class DeferredEntriesPlugin {
  private onBeforeDeferredEntries?: () => Promise<void>
  private deferredEntrypoints?: webpack.EntryObject
  private callbackCalled: boolean = false

  constructor(options: DeferredEntriesPluginOptions) {
    this.onBeforeDeferredEntries =
      options.config.experimental.onBeforeDeferredEntries
    this.deferredEntrypoints = options.deferredEntrypoints
  }

  apply(compiler: webpack.Compiler) {
    // Skip if no deferred entrypoints to process
    if (
      !this.deferredEntrypoints ||
      Object.keys(this.deferredEntrypoints).length === 0
    ) {
      return
    }

    // Use finishMake hook to add deferred entries after all initial entries are processed
    // This is the same pattern used by FlightClientEntryPlugin
    compiler.hooks.finishMake.tapPromise(PLUGIN_NAME, async (compilation) => {
      // Only process if we haven't called callback yet
      if (this.callbackCalled) {
        return
      }

      this.callbackCalled = true

      // Call the onBeforeDeferredEntries callback
      if (this.onBeforeDeferredEntries) {
        debug('calling onBeforeDeferredEntries callback')
        await this.onBeforeDeferredEntries()
        debug('onBeforeDeferredEntries callback completed')
      }

      // Add deferred entries to compilation
      const addEntryPromises: Promise<void>[] = []
      const bundler = webpack

      debug('adding deferred entries:', Object.keys(this.deferredEntrypoints!))

      for (const [name, entryData] of Object.entries(
        this.deferredEntrypoints!
      )) {
        debug('processing deferred entry:', name, entryData)
        // Normalize entry data structure
        let entry: {
          import?: string | string[]
          layer?: string
          runtime?: string | false
          dependOn?: string | string[]
        }

        if (typeof entryData === 'string') {
          entry = { import: [entryData] }
        } else if (Array.isArray(entryData)) {
          entry = { import: entryData }
        } else {
          entry = entryData as typeof entry
        }

        // Get imports array
        const imports = entry.import
          ? Array.isArray(entry.import)
            ? entry.import
            : [entry.import]
          : []

        if (imports.length === 0) {
          continue
        }

        // Normalize dependOn to always be an array
        const dependOn = entry.dependOn
          ? Array.isArray(entry.dependOn)
            ? entry.dependOn
            : [entry.dependOn]
          : undefined

        // Create dependencies for all imports
        for (const importPath of imports) {
          if (typeof importPath !== 'string') {
            continue
          }

          const dep = bundler.EntryPlugin.createDependency(importPath, {
            name,
          })

          addEntryPromises.push(
            new Promise((resolve, reject) => {
              compilation.addEntry(
                compiler.context,
                dep,
                {
                  name,
                  layer: entry.layer,
                  runtime: entry.runtime,
                  dependOn,
                },
                (err) => {
                  if (err) {
                    reject(err)
                  } else {
                    resolve()
                  }
                }
              )
            })
          )
        }
      }

      await Promise.all(addEntryPromises)
    })
  }
}
Quest for Codev2.0.0
/
SIGN IN