next.js/packages/next/src/build/turbopack-build/impl.ts
impl.ts359 lines11.5 KB
// Import cpu-profile first to start profiling early if enabled
import { saveCpuProfile } from '../../server/lib/cpu-profile'
import path from 'path'
import { validateTurboNextConfig } from '../../lib/turbopack-warning'
import { NextBuildContext } from '../build-context'
import { createDefineEnv, getBindingsSync } from '../swc'
import { installBindings } from '../swc/install-bindings'
import {
  handleRouteType,
  rawEntrypointsToEntrypoints,
} from '../handle-entrypoints'
import { TurbopackManifestLoader } from '../../shared/lib/turbopack/manifest-loader'
import { promises as fs } from 'fs'
import { PHASE_PRODUCTION_BUILD } from '../../shared/lib/constants'
import loadConfig from '../../server/config'
import { hasCustomExportOutput } from '../../export/utils'
import { Telemetry } from '../../telemetry/storage'
import {
  setGlobal,
  trace,
  initializeTraceState,
  getTraceEvents,
} from '../../trace'
import type { TraceState } from '../../trace'
import { isCI } from '../../server/ci-info'
import { backgroundLogCompilationEvents } from '../../shared/lib/turbopack/compilation-events'
import { getSupportedBrowsers } from '../get-supported-browsers'
import { printBuildErrors } from '../print-build-errors'
import { normalizePath } from '../../lib/normalize-path'
import type {
  ProjectOptions,
  RawEntrypoints,
  TurbopackResult,
} from '../swc/types'
import { Bundler } from '../../lib/bundler'

export async function turbopackBuild(): Promise<{
  duration: number
  buildTraceContext: undefined
  shutdownPromise: Promise<void>
}> {
  await validateTurboNextConfig({
    dir: NextBuildContext.dir!,
    configPhase: PHASE_PRODUCTION_BUILD,
  })

  const config = NextBuildContext.config!
  const dir = NextBuildContext.dir!
  const distDir = NextBuildContext.distDir!
  const buildId = NextBuildContext.buildId!
  const encryptionKey = NextBuildContext.encryptionKey!
  const previewProps = NextBuildContext.previewProps!
  const hasRewrites = NextBuildContext.hasRewrites!
  const rewrites = NextBuildContext.rewrites!
  const noMangling = NextBuildContext.noMangling!
  const currentNodeJsVersion = process.versions.node

  const startTime = process.hrtime()
  const bindings = getBindingsSync() // our caller should have already loaded these

  if (bindings.isWasm) {
    throw new Error(
      `Turbopack is not supported on this platform (${process.platform}/${process.arch}) because native bindings are not available. ` +
        `Only WebAssembly (WASM) bindings were loaded, and Turbopack requires native bindings.\n\n` +
        `To build on this platform, use Webpack instead:\n` +
        `  next build --webpack\n\n` +
        `For more information, see: https://nextjs.org/docs/app/api-reference/turbopack#supported-platforms`
    )
  }

  const dev = false

  const supportedBrowsers = getSupportedBrowsers(dir, dev)

  const hasDeferredEntries =
    (config.experimental.deferredEntries?.length ?? 0) > 0

  const persistentCaching =
    config.experimental?.turbopackFileSystemCacheForBuild || false
  const rootPath = config.turbopack?.root || config.outputFileTracingRoot || dir

  // Shared options for createProject calls
  const sharedProjectOptions: Omit<ProjectOptions, 'debugBuildPaths'> = {
    rootPath,
    projectPath: normalizePath(path.relative(rootPath, dir) || '.'),
    distDir,
    nextConfig: config,
    watch: {
      enable: false,
    },
    dev,
    env: process.env as Record<string, string>,
    defineEnv: createDefineEnv({
      isTurbopack: true,
      clientRouterFilters: NextBuildContext.clientRouterFilters!,
      config,
      dev,
      distDir,
      projectPath: dir,
      fetchCacheKeyPrefix: config.experimental.fetchCacheKeyPrefix,
      hasRewrites,
      // Implemented separately in Turbopack, doesn't have to be passed here.
      middlewareMatchers: undefined,
      rewrites,
    }),
    buildId,
    encryptionKey,
    previewProps,
    browserslistQuery: supportedBrowsers.join(', '),
    noMangling,
    writeRoutesHashesManifest:
      !!process.env.NEXT_TURBOPACK_WRITE_ROUTES_HASHES_MANIFEST,
    currentNodeJsVersion,
    isPersistentCachingEnabled: persistentCaching,
    deferredEntries: config.experimental.deferredEntries,
    nextVersion: process.env.__NEXT_VERSION as string,
  }

  const sharedTurboOptions = {
    memoryLimit: config.experimental?.turbopackMemoryLimit,
    dependencyTracking: persistentCaching || hasDeferredEntries,
    isCi: isCI,
    isShortSession: true,
    skipCompaction: process.env.NEXT_USE_POST_BUILD === '1',
  }

  const sriEnabled = Boolean(config.experimental.sri?.algorithm)

  const project = await bindings.turbo.createProject(
    {
      ...sharedProjectOptions,
      debugBuildPaths: NextBuildContext.debugBuildPaths,
    },
    sharedTurboOptions,
    hasDeferredEntries && config.experimental.onBeforeDeferredEntries
      ? {
          onBeforeDeferredEntries: async () => {
            const workerConfig = await loadConfig(PHASE_PRODUCTION_BUILD, dir, {
              debugPrerender: NextBuildContext.debugPrerender,
              reactProductionProfiling:
                NextBuildContext.reactProductionProfiling,
              bundler: Bundler.Turbopack,
            })

            await workerConfig.experimental.onBeforeDeferredEntries?.()
          },
        }
      : undefined
  )
  const buildEventsSpan = trace('turbopack-build-events')
  // Stop immediately: this span is only used as a parent for
  // manualTraceChild calls which carry their own timestamps.
  buildEventsSpan.stop()
  const shutdownController = new AbortController()
  const compilationEvents = backgroundLogCompilationEvents(project, {
    parentSpan: buildEventsSpan,
    signal: shutdownController.signal,
  })

  try {
    // Write an empty file in a known location to signal this was built with Turbopack
    await fs.writeFile(path.join(distDir, 'turbopack'), '')

    await fs.mkdir(path.join(distDir, 'server'), { recursive: true })
    await fs.mkdir(path.join(distDir, 'static', buildId), {
      recursive: true,
    })
    await fs.writeFile(
      path.join(distDir, 'package.json'),
      '{"type": "commonjs"}'
    )

    let appDirOnly = NextBuildContext.appDirOnly!

    const entrypoints = await project.writeAllEntrypointsToDisk(appDirOnly)
    printBuildErrors(entrypoints, dev)

    const routes = entrypoints.routes
    if (!routes) {
      // This should never ever happen, there should be an error issue, or the bindings call should
      // have thrown.
      throw new Error(`Turbopack build failed`)
    }

    const hasPagesEntries = Array.from(routes.values()).some((route) => {
      if (route.type === 'page' || route.type === 'page-api') {
        return true
      }
      return false
    })
    // If there's no pages entries, then we are in app-dir-only mode
    if (!hasPagesEntries) {
      appDirOnly = true
    }

    const manifestLoader = new TurbopackManifestLoader({
      buildId,
      distDir,
      encryptionKey,
      dev: false,
      sriEnabled,
    })

    const currentEntrypoints = await rawEntrypointsToEntrypoints(
      entrypoints as TurbopackResult<RawEntrypoints>
    )

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

    if (!appDirOnly) {
      for (const [page, route] of currentEntrypoints.page) {
        promises.push(
          handleRouteType({
            page,
            route,
            manifestLoader,
          })
        )
      }
    }

    for (const [page, route] of currentEntrypoints.app) {
      promises.push(
        handleRouteType({
          page,
          route,
          manifestLoader,
        })
      )
    }

    await Promise.all(promises)

    await Promise.all([
      // Only load pages router manifests if not app-only
      ...(!appDirOnly
        ? [
            manifestLoader.loadBuildManifest('_app'),
            manifestLoader.loadPagesManifest('_app'),
            manifestLoader.loadFontManifest('_app'),
            manifestLoader.loadPagesManifest('_document'),
            manifestLoader.loadClientBuildManifest('_error'),
            manifestLoader.loadBuildManifest('_error'),
            manifestLoader.loadPagesManifest('_error'),
            manifestLoader.loadFontManifest('_error'),
          ]
        : []),
      entrypoints.instrumentation &&
        manifestLoader.loadMiddlewareManifest(
          'instrumentation',
          'instrumentation'
        ),
      entrypoints.middleware &&
        (await manifestLoader.loadMiddlewareManifest(
          'middleware',
          'middleware'
        )),
    ])

    manifestLoader.writeManifests({
      devRewrites: undefined,
      productionRewrites: rewrites,
      entrypoints: currentEntrypoints,
    })

    if (NextBuildContext.analyze) {
      await project.writeAnalyzeData(appDirOnly)
    }

    // Shutdown may trigger final compilation events (e.g. persistence,
    // compaction trace spans).  This is the last chance to capture them.
    // After shutdown resolves we abort the signal to close the iterator
    // and drain any remaining buffered events.
    const shutdownPromise = project.shutdown().then(() => {
      shutdownController.abort()
      return compilationEvents.catch(() => {})
    })

    const time = process.hrtime(startTime)
    return {
      duration: time[0] + time[1] / 1e9,
      buildTraceContext: undefined,
      shutdownPromise,
    }
  } catch (err) {
    await project.shutdown()
    shutdownController.abort()
    await compilationEvents.catch(() => {})
    throw err
  }
}

let shutdownPromise: Promise<void> | undefined
export async function workerMain(workerData: {
  buildContext: typeof NextBuildContext
  traceState: TraceState & { shouldSaveTraceEvents: boolean }
}): Promise<
  Omit<Awaited<ReturnType<typeof turbopackBuild>>, 'shutdownPromise'>
> {
  // setup new build context from the serialized data passed from the parent
  Object.assign(NextBuildContext, workerData.buildContext)
  initializeTraceState(workerData.traceState)

  /// load the config because it's not serializable
  const config = await loadConfig(
    PHASE_PRODUCTION_BUILD,
    NextBuildContext.dir!,
    {
      debugPrerender: NextBuildContext.debugPrerender,
      reactProductionProfiling: NextBuildContext.reactProductionProfiling,
      bundler: Bundler.Turbopack,
    }
  )
  NextBuildContext.config = config
  // Matches handling in build/index.ts
  // https://github.com/vercel/next.js/blob/84f347fc86f4efc4ec9f13615c215e4b9fb6f8f0/packages/next/src/build/index.ts#L815-L818
  // Ensures the `config.distDir` option is matched.
  if (hasCustomExportOutput(NextBuildContext.config)) {
    NextBuildContext.config.distDir = '.next'
  }

  // Clone the telemetry for worker
  const telemetry = new Telemetry({
    distDir: NextBuildContext.config.distDir,
  })
  setGlobal('telemetry', telemetry)
  // Install bindings early so we can access synchronously later
  await installBindings(config.experimental?.useWasmBinary)

  try {
    const {
      shutdownPromise: resultShutdownPromise,
      buildTraceContext,
      duration,
    } = await turbopackBuild()
    shutdownPromise = resultShutdownPromise
    return {
      buildTraceContext,
      duration,
    }
  } finally {
    // Always flush telemetry before worker exits (waits for async operations like setTimeout in debug mode)
    await telemetry.flush()
    // Save CPU profile before worker exits
    await saveCpuProfile()
  }
}

export async function waitForShutdown(): Promise<{
  debugTraceEvents?: ReturnType<typeof getTraceEvents>
}> {
  if (shutdownPromise) {
    await shutdownPromise
  }
  // Collect trace events after shutdown completes so that all compilation
  // events (e.g. persistence trace spans) have been processed.
  return { debugTraceEvents: getTraceEvents() }
}
Quest for Codev2.0.0
/
SIGN IN