next.js/packages/next/src/server/dev/hot-reloader-turbopack.ts
hot-reloader-turbopack.ts1863 lines59.3 KB
import type { Socket } from 'net'
import { mkdir, writeFile } from 'fs/promises'
import * as inspector from 'inspector'
import { join, extname, relative } from 'path'
import { pathToFileURL } from 'url'

import ws from 'next/dist/compiled/ws'

import type { OutputState } from '../../build/output/store'
import { store as consoleStore } from '../../build/output/store'
import type {
  CompilationError,
  HmrMessageSentToBrowser,
  NextJsHotReloaderInterface,
  ReloadPageMessage,
  SyncMessage,
  TurbopackConnectedMessage,
} from './hot-reloader-types'
import { HMR_MESSAGE_SENT_TO_BROWSER } from './hot-reloader-types'
import type {
  Update as TurbopackUpdate,
  Endpoint,
  WrittenEndpoint,
  TurbopackResult,
  Project,
  Entrypoints,
  NodeJsHmrUpdate,
  NodeJsPartialHmrUpdate,
} from '../../build/swc/types'
import { createDefineEnv, getBindingsSync, HmrTarget } from '../../build/swc'
import * as Log from '../../build/output/log'
import { BLOCKED_PAGES } from '../../shared/lib/constants'
import {
  getOverlayMiddleware,
  getSourceMapMiddleware,
  getOriginalStackFrames,
} from './middleware-turbopack'
import { PageNotFoundError } from '../../shared/lib/utils'
import { debounce } from '../utils'
import { deleteCache } from './require-cache'
import {
  clearAllModuleContexts,
  clearModuleContext,
} from '../lib/render-server'
import { denormalizePagePath } from '../../shared/lib/page-path/denormalize-page-path'
import { trace } from '../../trace'
import {
  AssetMapper,
  type ChangeSubscriptions,
  type ClientState,
  handleEntrypoints,
  handlePagesErrorRoute,
  handleRouteType,
  hasEntrypointForKey,
  msToNs,
  type ReadyIds,
  type SendHmr,
  type StartBuilding,
  processTopLevelIssues,
  printNonFatalIssue,
  normalizedPageToTurbopackStructureRoute,
} from './turbopack-utils'
import {
  propagateServerField,
  type ServerFields,
  type SetupOpts,
} from '../lib/router-utils/setup-dev-bundler'
import { TurbopackManifestLoader } from '../../shared/lib/turbopack/manifest-loader'
import { findPagePathData } from './on-demand-entry-handler'
import type { RouteDefinition } from '../route-definitions/route-definition'
import {
  type EntryKey,
  getEntryKey,
  splitEntryKey,
} from '../../shared/lib/turbopack/entry-key'
import {
  createBinaryHmrMessageData,
  FAST_REFRESH_RUNTIME_RELOAD,
} from './messages'
import { generateEncryptionKeyBase64 } from '../app-render/encryption-utils-server'
import { isAppPageRouteDefinition } from '../route-definitions/app-page-route-definition'
import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
import type { ModernSourceMapPayload } from '../lib/source-maps'
import { isDeferredEntry } from '../../build/entries'
import { isMetadataRouteFile } from '../../lib/metadata/is-metadata-route'
import { setBundlerFindSourceMapImplementation } from '../patch-error-inspect'
import { getNextErrorFeedbackMiddleware } from '../../next-devtools/server/get-next-error-feedback-middleware'
import {
  formatIssue,
  isFileSystemCacheEnabledForDev,
  isWellKnownError,
  processIssues,
  renderStyledStringToErrorAnsi,
  type EntryIssuesMap,
  type IssuesMap,
  type TopLevelIssuesMap,
} from '../../shared/lib/turbopack/utils'
import { getDevOverlayFontMiddleware } from '../../next-devtools/server/font/get-dev-overlay-font-middleware'
import { devIndicatorServerState } from './dev-indicator-server-state'
import { getDisableDevIndicatorMiddleware } from '../../next-devtools/server/dev-indicator-middleware'
import { getRestartDevServerMiddleware } from '../../next-devtools/server/restart-dev-server-middleware'
import { backgroundLogCompilationEvents } from '../../shared/lib/turbopack/compilation-events'
import { getSupportedBrowsers } from '../../build/get-supported-browsers'
import { printBuildErrors } from '../../build/print-build-errors'
import { receiveBrowserLogsTurbopack } from './browser-logs/receive-logs'
import { normalizePath } from '../../lib/normalize-path'
import {
  devToolsConfigMiddleware,
  getDevToolsConfig,
} from '../../next-devtools/server/devtools-config-middleware'
import { getAttachNodejsDebuggerMiddleware } from '../../next-devtools/server/attach-nodejs-debugger-middleware'
import {
  connectReactDebugChannel,
  connectReactDebugChannelForHtmlRequest,
  deleteReactDebugChannelForHtmlRequest,
  setReactDebugChannelForHtmlRequest,
} from './debug-channel'
import {
  getVersionInfo,
  matchNextPageBundleRequest,
} from './hot-reloader-shared-utils'
import { getMcpMiddleware } from '../mcp/get-mcp-middleware'
import { handleErrorStateResponse } from '../mcp/tools/get-errors'
import { handlePageMetadataResponse } from '../mcp/tools/get-page-metadata'
import { setStackFrameResolver } from '../mcp/tools/utils/format-errors'
import { recordMcpTelemetry } from '../mcp/mcp-telemetry-tracker'
import { getFileLogger } from './browser-logs/file-logger'
import type { ServerCacheStatus } from '../../next-devtools/dev-overlay/cache-indicator'
import type { Lockfile } from '../../build/lockfile'
import {
  sendSerializedErrorsToClient,
  sendSerializedErrorsToClientForHtmlRequest,
  setErrorsRscStreamForHtmlRequest,
} from './serialized-errors'

const wsServer = new ws.Server({ noServer: true })
const isTestMode = !!(
  process.env.NEXT_TEST_MODE ||
  process.env.__NEXT_TEST_MODE ||
  process.env.DEBUG
)

const sessionId = Math.floor(Number.MAX_SAFE_INTEGER * Math.random())

declare const __next__clear_chunk_cache__: (() => void) | null | undefined

declare const __turbopack_server_hmr_apply__:
  | ((update: NodeJsPartialHmrUpdate) => boolean)
  | undefined

type ServerHmrSubscriptions = Map<
  string,
  AsyncIterableIterator<TurbopackResult<NodeJsHmrUpdate>>
>

function setupServerHmr(
  project: Project,
  {
    clear,
  }: {
    clear: () => void | Promise<void>
  }
) {
  const serverHmrSubscriptions: ServerHmrSubscriptions = new Map()

  /**
   * Subscribe to HMR updates for a server chunk.
   * @param chunkPath - Server chunk output path (e.g., "server/chunks/ssr/..._.js")
   */
  function subscribeToServerHmr(chunkPath: string) {
    if (serverHmrSubscriptions.has(chunkPath)) {
      return
    }

    const subscription = project.hmrEvents(chunkPath, HmrTarget.Server)
    serverHmrSubscriptions.set(chunkPath, subscription)

    // Start listening for changes in background
    ;(async () => {
      // Skip initial state
      await subscription.next()

      for await (const result of subscription) {
        const update = result as NodeJsHmrUpdate

        // Fully re-evaluate all chunks from disk. Clears the module cache and
        // notifies browsers to refetch RSC.
        if (update.type === 'restart') {
          await clear()
          continue
        }

        if (update.type !== 'partial') {
          continue
        }

        const instruction = update.instruction
        if (!instruction || instruction.type !== 'EcmascriptMergedUpdate') {
          continue
        }

        if (typeof __turbopack_server_hmr_apply__ === 'function') {
          const applied = __turbopack_server_hmr_apply__(update)
          if (!applied) {
            await clear()
          }
        }
      }
    })().catch(async (err) => {
      console.error('[Server HMR] Subscription error:', err)
      serverHmrSubscriptions.delete(chunkPath)
      await clear()
    })
  }

  // Listen to the Rust bindings update us on changing server HMR chunk paths
  ;(async () => {
    try {
      const serverHmrChunkPaths = project.hmrChunkNamesSubscribe(
        HmrTarget.Server
      )

      // Process chunk paths (both initial and subsequent updates)
      for await (const data of serverHmrChunkPaths) {
        const currentChunkPaths = new Set<string>(
          data.chunkNames.filter((path) => path.endsWith('.js'))
        )

        // Clean up subscriptions for removed chunk paths (like when pages are deleted)
        const chunkPathsToRemove: string[] = []
        for (const chunkPath of serverHmrSubscriptions.keys()) {
          if (!currentChunkPaths.has(chunkPath)) {
            chunkPathsToRemove.push(chunkPath)
          }
        }

        for (const chunkPath of chunkPathsToRemove) {
          const subscription = serverHmrSubscriptions.get(chunkPath)
          subscription?.return?.()
          serverHmrSubscriptions.delete(chunkPath)
        }

        // Subscribe to HMR events for new server chunks
        for (const chunkPath of currentChunkPaths) {
          if (!serverHmrSubscriptions.has(chunkPath)) {
            subscribeToServerHmr(chunkPath)
          }
        }
      }
    } catch (err) {
      console.error('[Server HMR Setup] Error in chunk path subscription:', err)
    }
  })()

  return serverHmrSubscriptions
}

/**
 * Replaces turbopack:///[project] with the specified project in the `source` field.
 */
function rewriteTurbopackSources(
  projectRoot: string,
  sourceMap: ModernSourceMapPayload
): void {
  if ('sections' in sourceMap) {
    for (const section of sourceMap.sections) {
      rewriteTurbopackSources(projectRoot, section.map)
    }
  } else {
    for (let i = 0; i < sourceMap.sources.length; i++) {
      sourceMap.sources[i] = pathToFileURL(
        join(
          projectRoot,
          sourceMap.sources[i].replace(/turbopack:\/\/\/\[project\]/, '')
        )
      ).toString()
    }
  }
}

function getSourceMapFromTurbopack(
  project: Project,
  projectRoot: string,
  sourceURL: string
): ModernSourceMapPayload | undefined {
  let sourceMapJson: string | null = null

  try {
    sourceMapJson = project.getSourceMapSync(sourceURL)
  } catch (err) {}

  if (sourceMapJson === null) {
    return undefined
  } else {
    const payload: ModernSourceMapPayload = JSON.parse(sourceMapJson)
    // The sourcemap from Turbopack is not yet written to disk so its `sources`
    // are not absolute paths yet. We need to rewrite them to be absolute paths.
    rewriteTurbopackSources(projectRoot, payload)
    return payload
  }
}

export async function createHotReloaderTurbopack(
  opts: SetupOpts & { isSrcDir: boolean },
  serverFields: ServerFields,
  distDir: string,
  resetFetch: () => void,
  lockfile: Lockfile | undefined,
  serverFastRefresh?: boolean
): Promise<NextJsHotReloaderInterface> {
  const dev = true
  const buildId = 'development'
  const { nextConfig, dir: projectPath } = opts

  const bindings = getBindingsSync()

  // Turbopack requires native bindings and cannot run with WASM bindings.
  // Detect this early and give a clear, actionable error message.
  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 use Next.js on this platform, use Webpack instead:\n` +
        `  next dev --webpack\n\n` +
        `For more information, see: https://nextjs.org/docs/app/api-reference/turbopack#supported-platforms`
    )
  }

  // For the debugging purpose, check if createNext or equivalent next instance setup in test cases
  // works correctly. Normally `run-test` hides output so only will be visible when `--debug` flag is used.
  if (isTestMode) {
    ;(require('console') as typeof import('console')).log(
      'Creating turbopack project',
      {
        dir: projectPath,
        testMode: isTestMode,
      }
    )
  }

  const hasRewrites =
    opts.fsChecker.rewrites.afterFiles.length > 0 ||
    opts.fsChecker.rewrites.beforeFiles.length > 0 ||
    opts.fsChecker.rewrites.fallback.length > 0

  const hotReloaderSpan = trace('hot-reloader', undefined, {
    version: process.env.__NEXT_VERSION as string,
  })
  // Ensure the hotReloaderSpan is flushed immediately as it's the parentSpan for all processing
  // of the current `next dev` invocation.
  hotReloaderSpan.stop()

  // Initialize log monitor for file logging
  // Enable logging by default in development mode
  const mcpServerEnabled = !!nextConfig.experimental.mcpServer
  const fileLogger = getFileLogger()
  fileLogger.initialize(distDir, mcpServerEnabled)

  const encryptionKey = await generateEncryptionKeyBase64({
    isBuild: false,
    distDir,
  })

  // TODO: Implement
  let clientRouterFilters: any
  if (nextConfig.experimental.clientRouterFilter) {
    // TODO this need to be set correctly for filesystem cache to work
  }

  const supportedBrowsers = getSupportedBrowsers(projectPath, dev)
  const currentNodeJsVersion = process.versions.node

  const rootPath =
    opts.nextConfig.turbopack?.root ||
    opts.nextConfig.outputFileTracingRoot ||
    projectPath
  const project = await bindings.turbo.createProject(
    {
      rootPath,
      projectPath: normalizePath(relative(rootPath, projectPath) || '.'),
      distDir,
      nextConfig: opts.nextConfig,
      watch: {
        enable: dev,
        pollIntervalMs: nextConfig.watchOptions?.pollIntervalMs,
      },
      dev,
      env: process.env as Record<string, string>,
      defineEnv: createDefineEnv({
        isTurbopack: true,
        clientRouterFilters,
        config: nextConfig,
        dev,
        distDir,
        projectPath,
        fetchCacheKeyPrefix: opts.nextConfig.experimental.fetchCacheKeyPrefix,
        hasRewrites,
        // TODO: Implement
        middlewareMatchers: undefined,
        rewrites: opts.fsChecker.rewrites,
      }),
      buildId,
      encryptionKey,
      previewProps: opts.fsChecker.previewProps,
      browserslistQuery: supportedBrowsers.join(', '),
      noMangling: false,
      writeRoutesHashesManifest: false,
      currentNodeJsVersion,
      isPersistentCachingEnabled: isFileSystemCacheEnabledForDev(
        opts.nextConfig
      ),
      nextVersion: process.env.__NEXT_VERSION as string,
      serverHmr: serverFastRefresh,
    },
    {
      memoryLimit: opts.nextConfig.experimental?.turbopackMemoryLimit,
      isShortSession: false,
    }
  )
  backgroundLogCompilationEvents(project, {
    eventTypes: [
      'StartupCacheInvalidationEvent',
      'TimingEvent',
      'SlowFilesystemEvent',
      'TraceEvent',
    ],
    parentSpan: hotReloaderSpan,
  })
  setBundlerFindSourceMapImplementation(
    getSourceMapFromTurbopack.bind(null, project, projectPath)
  )

  // Set up code frame renderer using native bindings
  const { installCodeFrameSupport } =
    require('../lib/install-code-frame') as typeof import('../lib/install-code-frame')
  installCodeFrameSupport()

  opts.onDevServerCleanup?.(async () => {
    setBundlerFindSourceMapImplementation(() => undefined)
    await project.onExit()
    await lockfile?.unlock()
  })
  const entrypointsSubscription = project.entrypointsSubscribe()

  const currentWrittenEntrypoints: Map<EntryKey, WrittenEndpoint> = new Map()
  const currentEntrypoints: Entrypoints = {
    global: {
      app: undefined,
      document: undefined,
      error: undefined,

      middleware: undefined,
      instrumentation: undefined,
    },

    page: new Map(),
    app: new Map(),
  }

  const currentTopLevelIssues: TopLevelIssuesMap = new Map()
  const currentEntryIssues: EntryIssuesMap = new Map()

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

  // Dev specific
  const changeSubscriptions: ChangeSubscriptions = new Map()
  const serverPathState = new Map<string, string>()
  const readyIds: ReadyIds = new Set()
  let currentEntriesHandlingResolve: ((value?: unknown) => void) | undefined
  let currentEntriesHandling = new Promise(
    (resolve) => (currentEntriesHandlingResolve = resolve)
  )

  const assetMapper = new AssetMapper()

  // Deferred entries state management
  const deferredEntriesConfig = nextConfig.experimental.deferredEntries
  const hasDeferredEntriesConfig =
    deferredEntriesConfig && deferredEntriesConfig.length > 0
  let onBeforeDeferredEntriesCalled = false
  let onBeforeDeferredEntriesPromise: Promise<void> | null = null
  // Track non-deferred entries that are currently being built
  const nonDeferredBuildingEntries: Set<string> = new Set()

  // Function to wait for all non-deferred entries to be built
  async function waitForNonDeferredEntries(): Promise<void> {
    return new Promise<void>((resolve) => {
      const checkEntries = () => {
        // Check if there are any non-deferred entries that are still building
        if (nonDeferredBuildingEntries.size === 0) {
          resolve()
        } else {
          // Check again after a short delay
          setTimeout(checkEntries, 100)
        }
      }
      checkEntries()
    })
  }

  // Function to handle deferred entry processing
  async function processDeferredEntry(): Promise<void> {
    if (!hasDeferredEntriesConfig) return

    // Wait for all non-deferred entries to be built
    await waitForNonDeferredEntries()

    // Call the onBeforeDeferredEntries callback once
    if (!onBeforeDeferredEntriesCalled) {
      onBeforeDeferredEntriesCalled = true

      if (nextConfig.experimental.onBeforeDeferredEntries) {
        if (!onBeforeDeferredEntriesPromise) {
          onBeforeDeferredEntriesPromise =
            nextConfig.experimental.onBeforeDeferredEntries()
        }
        await onBeforeDeferredEntriesPromise
      }
    } else if (onBeforeDeferredEntriesPromise) {
      // Wait for any in-progress callback
      await onBeforeDeferredEntriesPromise
    }
  }

  // Track whether HMR is pending - used to call callback once after HMR settles
  let hmrPendingDeferredCallback = false

  // Debounced function to call onBeforeDeferredEntries after HMR
  // This prevents rapid-fire calls when turbopack fires many update events
  // Use 500ms debounce to ensure all rapid updates are batched together
  const callOnBeforeDeferredEntriesAfterHMR = debounce(() => {
    // Only call if HMR triggered a need for the callback
    if (hasDeferredEntriesConfig && hmrPendingDeferredCallback) {
      hmrPendingDeferredCallback = false
      onBeforeDeferredEntriesCalled = true
      if (nextConfig.experimental.onBeforeDeferredEntries) {
        onBeforeDeferredEntriesPromise =
          nextConfig.experimental.onBeforeDeferredEntries()
      }
    }
  }, 500)

  function clearRequireCache(
    key: EntryKey,
    writtenEndpoint: WrittenEndpoint,
    {
      force,
    }: {
      // Always clear the cache, don't check if files have changed
      force?: boolean
    } = {}
  ): boolean {
    if (force) {
      for (const { path, contentHash } of writtenEndpoint.serverPaths) {
        // We ignore source maps
        if (path.endsWith('.map')) continue
        const localKey = `${key}:${path}`
        serverPathState.set(localKey, contentHash)
        serverPathState.set(path, contentHash)
      }
    } else {
      // Figure out if the server files have changed
      let hasChange = false
      for (const { path, contentHash } of writtenEndpoint.serverPaths) {
        // We ignore source maps
        if (path.endsWith('.map')) continue
        const localKey = `${key}:${path}`
        const localHash = serverPathState.get(localKey)
        const globalHash = serverPathState.get(path)
        if (
          (localHash && localHash !== contentHash) ||
          (globalHash && globalHash !== contentHash)
        ) {
          hasChange = true
          serverPathState.set(localKey, contentHash)
          serverPathState.set(path, contentHash)
        } else {
          if (!localHash) {
            serverPathState.set(localKey, contentHash)
          }
          if (!globalHash) {
            serverPathState.set(path, contentHash)
          }
        }
      }

      if (!hasChange) {
        return false
      }
    }

    const serverPaths = writtenEndpoint.serverPaths.map(({ path: p }) =>
      join(distDir, p)
    )

    const { type: entryType } = splitEntryKey(key)

    // Server HMR applies to App Router entries built with the Turbopack Node.js
    // runtime: app pages and route handlers (including metadata routes). Edge
    // routes, Pages Router pages, and middleware/instrumentation are excluded.
    const usesServerHmr =
      serverFastRefresh &&
      entryType === 'app' &&
      writtenEndpoint.type !== 'edge'

    const filesToDelete: string[] = []
    for (const file of serverPaths) {
      clearModuleContext(file)

      const relativePath = relative(distDir, file)
      if (
        // For Pages Router, edge routes, middleware, and any entry not
        // participating in server HMR: clear the sharedCache in
        // evalManifest(), Node.js require.cache, and edge runtime module
        // contexts.
        force ||
        !usesServerHmr ||
        !serverHmrSubscriptions?.has(relativePath)
      ) {
        filesToDelete.push(file)
      }
    }
    deleteCache(filesToDelete)

    // Reset the fetch patch so patchFetch() can re-wrap on the next request.
    if (serverPaths.length > 0) {
      resetFetch()
    }

    // Clear Turbopack's chunk-loading cache so chunks are re-required from disk on
    // the next request.
    //
    // For App Router with server HMR, this is normally skipped as server HMR
    // manages module updates in-place. However, it *is* required when force is `true`
    // (like for .env file or tsconfig changes).
    if (
      (!usesServerHmr || force) &&
      typeof __next__clear_chunk_cache__ === 'function'
    ) {
      __next__clear_chunk_cache__()
    }

    return true
  }

  const buildingIds = new Set()

  const startBuilding: StartBuilding = (id, requestUrl, forceRebuild) => {
    if (!forceRebuild && readyIds.has(id)) {
      return () => {}
    }
    if (buildingIds.size === 0) {
      consoleStore.setState(
        {
          loading: true,
          trigger: id,
          url: requestUrl,
        } as OutputState,
        true
      )
    }
    buildingIds.add(id)
    return function finishBuilding() {
      if (buildingIds.size === 0) {
        return
      }
      readyIds.add(id)
      buildingIds.delete(id)
      if (buildingIds.size === 0) {
        hmrEventHappened = false
        consoleStore.setState(
          {
            loading: false,
          } as OutputState,
          true
        )
      }
    }
  }

  let serverHmrSubscriptions: ServerHmrSubscriptions | undefined

  let hmrEventHappened = false
  let hmrHash = 0

  const clientsWithoutHtmlRequestId = new Set<ws>()
  const clientsByHtmlRequestId = new Map<string, ws>()
  const cacheStatusesByHtmlRequestId = new Map<string, ServerCacheStatus>()
  const clientStates = new WeakMap<ws, ClientState>()

  function sendToClient(client: ws, message: HmrMessageSentToBrowser) {
    const data =
      typeof message.type === 'number'
        ? createBinaryHmrMessageData(message)
        : JSON.stringify(message)

    client.send(data)
  }

  function sendEnqueuedMessages() {
    for (const [, issueMap] of currentEntryIssues) {
      if (
        [...issueMap.values()].filter((i) => i.severity !== 'warning').length >
        0
      ) {
        // During compilation errors we want to delay the HMR events until errors are fixed
        return
      }
    }

    for (const client of [
      ...clientsWithoutHtmlRequestId,
      ...clientsByHtmlRequestId.values(),
    ]) {
      const state = clientStates.get(client)
      if (!state) {
        continue
      }

      for (const [, issueMap] of state.clientIssues) {
        if (
          [...issueMap.values()].filter((i) => i.severity !== 'warning')
            .length > 0
        ) {
          // During compilation errors we want to delay the HMR events until errors are fixed
          return
        }
      }

      for (const message of state.messages.values()) {
        sendToClient(client, message)
      }
      state.messages.clear()

      if (state.turbopackUpdates.length > 0) {
        sendToClient(client, {
          type: HMR_MESSAGE_SENT_TO_BROWSER.TURBOPACK_MESSAGE,
          data: state.turbopackUpdates,
        })
        state.turbopackUpdates.length = 0
      }
    }
  }
  const sendEnqueuedMessagesDebounce = debounce(sendEnqueuedMessages, 2)

  const sendHmr: SendHmr = (id: string, message: HmrMessageSentToBrowser) => {
    for (const client of [
      ...clientsWithoutHtmlRequestId,
      ...clientsByHtmlRequestId.values(),
    ]) {
      clientStates.get(client)?.messages.set(id, message)
    }

    hmrEventHappened = true
    sendEnqueuedMessagesDebounce()
  }

  function sendTurbopackMessage(payload: TurbopackUpdate) {
    // TODO(PACK-2049): For some reason we end up emitting hundreds of issues messages on bigger apps,
    //   a lot of which are duplicates.
    //   They are currently not handled on the client at all, so might as well not send them for now.
    payload.diagnostics = []
    payload.issues = []

    for (const client of [
      ...clientsWithoutHtmlRequestId,
      ...clientsByHtmlRequestId.values(),
    ]) {
      clientStates.get(client)?.turbopackUpdates.push(payload)
    }

    hmrEventHappened = true
    sendEnqueuedMessagesDebounce()
  }

  async function subscribeToClientChanges(
    key: EntryKey,
    includeIssues: boolean,
    endpoint: Endpoint,
    createMessage: (
      change: TurbopackResult,
      hash: string
    ) => Promise<HmrMessageSentToBrowser> | HmrMessageSentToBrowser | void,
    onError?: (
      error: Error
    ) => Promise<HmrMessageSentToBrowser> | HmrMessageSentToBrowser | void
  ) {
    if (changeSubscriptions.has(key)) {
      return
    }

    const { side } = splitEntryKey(key)

    const changedPromise = endpoint[`${side}Changed`](includeIssues)
    changeSubscriptions.set(key, changedPromise)
    try {
      const changed = await changedPromise

      for await (const change of changed) {
        processIssues(currentEntryIssues, key, change, false, true)
        // TODO: Get an actual content hash from Turbopack.
        const message = await createMessage(change, String(++hmrHash))
        if (message) {
          sendHmr(key, message)
        }
      }
    } catch (e) {
      changeSubscriptions.delete(key)
      const payload = await onError?.(e as Error)
      if (payload) {
        sendHmr(key, payload)
      }
      return
    }
    changeSubscriptions.delete(key)
  }

  async function unsubscribeFromClientChanges(key: EntryKey) {
    const subscription = await changeSubscriptions.get(key)
    if (subscription) {
      await subscription.return?.()
      changeSubscriptions.delete(key)
    }
    currentEntryIssues.delete(key)
  }

  async function subscribeToClientHmrEvents(client: ws, id: string) {
    const key = getEntryKey('assets', 'client', id)
    if (!hasEntrypointForKey(currentEntrypoints, key, assetMapper)) {
      // maybe throw an error / force the client to reload?
      return
    }

    const state = clientStates.get(client)
    if (!state || state.subscriptions.has(id)) {
      return
    }

    const subscription = project!.hmrEvents(id, HmrTarget.Client)
    state.subscriptions.set(id, subscription)

    // The subscription will always emit once, which is the initial
    // computation. This is not a change, so swallow it.
    try {
      await subscription.next()

      for await (const data of subscription) {
        processIssues(state.clientIssues, key, data, false, true)
        if (data.type !== 'issues') {
          sendTurbopackMessage(data as TurbopackUpdate)
        }
      }
    } catch (e) {
      // The client might be using an HMR session from a previous server, tell them
      // to fully reload the page to resolve the issue. We can't use
      // `hotReloader.send` since that would force every connected client to
      // reload, only this client is out of date.
      const reloadMessage: ReloadPageMessage = {
        type: HMR_MESSAGE_SENT_TO_BROWSER.RELOAD_PAGE,
        data: `error in HMR event subscription for ${id}: ${e}`,
      }
      sendToClient(client, reloadMessage)
      client.close()
      return
    }
  }

  function unsubscribeFromClientHmrEvents(client: ws, id: string) {
    const state = clientStates.get(client)
    if (!state) {
      return
    }

    const subscription = state.subscriptions.get(id)
    subscription?.return!()

    const key = getEntryKey('assets', 'client', id)
    state.clientIssues.delete(key)
  }

  async function handleEntrypointsSubscription() {
    for await (const entrypoints of entrypointsSubscription) {
      if (!currentEntriesHandlingResolve) {
        currentEntriesHandling = new Promise(
          // eslint-disable-next-line no-loop-func
          (resolve) => (currentEntriesHandlingResolve = resolve)
        )
      }

      // Always process issues/diagnostics, even if there are no entrypoints yet
      processTopLevelIssues(currentTopLevelIssues, entrypoints)

      // Certain crtical issues prevent any entrypoints from being constructed so return early
      if (!('routes' in entrypoints)) {
        printBuildErrors(entrypoints, true)

        currentEntriesHandlingResolve!()
        currentEntriesHandlingResolve = undefined
        continue
      }

      const routes = entrypoints.routes
      const existingRoutes = [
        ...currentEntrypoints.app.keys(),
        ...currentEntrypoints.page.keys(),
      ]
      const newRoutes = [...routes.keys()]

      const addedRoutes = newRoutes.filter(
        (route) =>
          !currentEntrypoints.app.has(route) &&
          !currentEntrypoints.page.has(route)
      )
      const removedRoutes = existingRoutes.filter((route) => !routes.has(route))

      await handleEntrypoints({
        entrypoints: entrypoints as any,

        currentEntrypoints,

        currentEntryIssues,
        manifestLoader,
        devRewrites: opts.fsChecker.rewrites,
        productionRewrites: undefined,
        logErrors: true,

        dev: {
          assetMapper,
          changeSubscriptions,
          clients: [
            ...clientsWithoutHtmlRequestId,
            ...clientsByHtmlRequestId.values(),
          ],
          clientStates,
          serverFields,

          hooks: {
            handleWrittenEndpoint: (id, result, forceDeleteCache) => {
              currentWrittenEntrypoints.set(id, result)
              return clearRequireCache(id, result, { force: forceDeleteCache })
            },
            propagateServerField: propagateServerField.bind(null, opts),
            sendHmr,
            startBuilding,
            subscribeToChanges: subscribeToClientChanges,
            unsubscribeFromChanges: unsubscribeFromClientChanges,
            unsubscribeFromHmrEvents: unsubscribeFromClientHmrEvents,
          },
        },
      })

      // Reload matchers when the files have been compiled
      await propagateServerField(opts, 'reloadMatchers', undefined)

      if (addedRoutes.length > 0 || removedRoutes.length > 0) {
        // When the list of routes changes a new manifest should be fetched for Pages Router.
        hotReloader.send({
          type: HMR_MESSAGE_SENT_TO_BROWSER.DEV_PAGES_MANIFEST_UPDATE,
          data: [
            {
              devPagesManifest: true,
            },
          ],
        })
      }

      for (const route of addedRoutes) {
        hotReloader.send({
          type: HMR_MESSAGE_SENT_TO_BROWSER.ADDED_PAGE,
          data: [route],
        })
      }

      for (const route of removedRoutes) {
        hotReloader.send({
          type: HMR_MESSAGE_SENT_TO_BROWSER.REMOVED_PAGE,
          data: [route],
        })
      }

      currentEntriesHandlingResolve!()
      currentEntriesHandlingResolve = undefined
    }
  }

  await mkdir(join(distDir, 'server'), { recursive: true })
  await mkdir(join(distDir, 'static', buildId), { recursive: true })
  await writeFile(
    join(distDir, 'package.json'),
    JSON.stringify(
      {
        type: 'commonjs',
      },
      null,
      2
    )
  )

  const middlewares = [
    getOverlayMiddleware({
      project,
      projectPath,
      isSrcDir: opts.isSrcDir,
    }),
    getSourceMapMiddleware(project),
    getNextErrorFeedbackMiddleware(opts.telemetry),
    getDevOverlayFontMiddleware(),
    getDisableDevIndicatorMiddleware(),
    getRestartDevServerMiddleware({
      telemetry: opts.telemetry,
      turbopackProject: project,
    }),
    devToolsConfigMiddleware({
      distDir,
      sendUpdateSignal: (data) => {
        hotReloader.send({
          type: HMR_MESSAGE_SENT_TO_BROWSER.DEVTOOLS_CONFIG,
          data,
        })
      },
    }),
    getAttachNodejsDebuggerMiddleware(),
    ...(nextConfig.experimental.mcpServer
      ? [
          getMcpMiddleware({
            projectPath,
            distDir,
            nextConfig,
            pagesDir: opts.pagesDir,
            appDir: opts.appDir,
            sendHmrMessage: (message) => hotReloader.send(message),
            getActiveConnectionCount: () =>
              clientsWithoutHtmlRequestId.size + clientsByHtmlRequestId.size,
            getDevServerUrl: () => process.env.__NEXT_PRIVATE_ORIGIN,
            getTurbopackProject: () => project,
          }),
        ]
      : []),
  ]

  setStackFrameResolver(async (request) => {
    return getOriginalStackFrames({
      project,
      projectPath,
      isServer: request.isServer,
      isEdgeServer: request.isEdgeServer,
      isAppDirectory: request.isAppDirectory,
      frames: request.frames,
    })
  })

  let versionInfoCached: ReturnType<typeof getVersionInfo> | undefined
  // This fetch, even though not awaited, is not kicked off eagerly because the first `fetch()` in
  // Node.js adds roughly 20ms main-thread blocking to load the SSL certificate cache
  // We don't want that blocking time to be in the hot path for the `ready in` logging.
  // Instead, the fetch is kicked off lazily when the first `getVersionInfoCached()` is called.
  const getVersionInfoCached = (): ReturnType<typeof getVersionInfo> => {
    if (!versionInfoCached) {
      versionInfoCached = getVersionInfo()
    }
    return versionInfoCached
  }

  let devtoolsFrontendUrl: string | undefined
  const inspectorURLRaw = inspector.url()
  if (inspectorURLRaw !== undefined) {
    const inspectorURL = new URL(inspectorURLRaw)

    let debugInfo
    try {
      const debugInfoList = await fetch(
        `http://${inspectorURL.host}/json/list`
      ).then((res) => res.json())
      debugInfo = debugInfoList[0]
    } catch {}
    if (debugInfo) {
      devtoolsFrontendUrl = debugInfo.devtoolsFrontendUrl
    }
  }

  const hotReloader: NextJsHotReloaderInterface = {
    turbopackProject: project,
    activeWebpackConfigs: undefined,
    serverStats: null,
    edgeServerStats: null,
    async run(req, res, _parsedUrl) {
      // intercept page chunks request and ensure them with turbopack
      if (req.url?.startsWith('/_next/static/chunks/pages/')) {
        const params = matchNextPageBundleRequest(req.url)

        if (params) {
          const decodedPagePath = `/${params.path
            .map((param: string) => decodeURIComponent(param))
            .join('/')}`

          const denormalizedPagePath = denormalizePagePath(decodedPagePath)

          await hotReloader
            .ensurePage({
              page: denormalizedPagePath,
              clientOnly: false,
              definition: undefined,
              url: req.url,
            })
            .catch(console.error)
        }
      }

      for (const middleware of middlewares) {
        let calledNext = false

        await middleware(req, res, () => {
          calledNext = true
        })

        if (!calledNext) {
          return { finished: true }
        }
      }

      // Request was not finished.
      return { finished: undefined }
    },

    // TODO: Figure out if socket type can match the NextJsHotReloaderInterface
    onHMR(req, socket: Socket, head, onUpgrade) {
      wsServer.handleUpgrade(req, socket, head, (client) => {
        const clientIssues: EntryIssuesMap = new Map()
        const subscriptions: Map<string, AsyncIterator<any>> = new Map()

        const htmlRequestId = req.url
          ? new URL(req.url, 'http://n').searchParams.get('id')
          : null

        // Clients with a request ID are inferred App Router clients. If Cache
        // Components is not enabled, we consider those legacy clients. Pages
        // Router clients are also considered legacy clients. TODO: Maybe mark
        // clients as App Router / Pages Router clients explicitly, instead of
        // inferring it from the presence of a request ID.
        if (htmlRequestId) {
          clientsByHtmlRequestId.set(htmlRequestId, client)
          const enableCacheComponents = nextConfig.cacheComponents
          if (enableCacheComponents) {
            onUpgrade(client, { isLegacyClient: false })
            const cacheStatus = cacheStatusesByHtmlRequestId.get(htmlRequestId)
            if (cacheStatus !== undefined) {
              sendToClient(client, {
                type: HMR_MESSAGE_SENT_TO_BROWSER.CACHE_INDICATOR,
                state: cacheStatus,
              })
              cacheStatusesByHtmlRequestId.delete(htmlRequestId)
            }
          } else {
            onUpgrade(client, { isLegacyClient: true })
          }

          connectReactDebugChannelForHtmlRequest(
            htmlRequestId,
            sendToClient.bind(null, client)
          )

          sendSerializedErrorsToClientForHtmlRequest(
            htmlRequestId,
            sendToClient.bind(null, client)
          )
        } else {
          clientsWithoutHtmlRequestId.add(client)
          onUpgrade(client, { isLegacyClient: true })
        }

        clientStates.set(client, {
          clientIssues,
          messages: new Map(),
          turbopackUpdates: [],
          subscriptions,
        })

        client.on('close', () => {
          // Remove active subscriptions
          for (const subscription of subscriptions.values()) {
            subscription.return?.()
          }
          clientStates.delete(client)

          if (htmlRequestId) {
            clientsByHtmlRequestId.delete(htmlRequestId)
            deleteReactDebugChannelForHtmlRequest(htmlRequestId)
          } else {
            clientsWithoutHtmlRequestId.delete(client)
          }
        })

        client.addEventListener('message', async ({ data }) => {
          const parsedData = JSON.parse(
            typeof data !== 'string' ? data.toString() : data
          )

          // Next.js messages
          switch (parsedData.event) {
            case 'span-end': {
              hotReloaderSpan.manualTraceChild(
                parsedData.spanName,
                msToNs(parsedData.startTime),
                msToNs(parsedData.endTime),
                parsedData.attributes
              )
              break
            }
            case 'client-hmr-latency': // { id, startTime, endTime, page, updatedModules, isPageHidden }
              hotReloaderSpan.manualTraceChild(
                parsedData.event,
                msToNs(parsedData.startTime),
                msToNs(parsedData.endTime),
                {
                  updatedModules: parsedData.updatedModules,
                  page: parsedData.page,
                  isPageHidden: parsedData.isPageHidden,
                }
              )
              break

            case 'client-error': // { errorCount, clientId }
            case 'client-warning': // { warningCount, clientId }
            case 'client-success': // { clientId }
            case 'server-component-reload-page': // { clientId }
            case 'client-reload-page': // { clientId }
            case 'client-removed-page': // { page }
            case 'client-full-reload': // { stackTrace, hadRuntimeError }
              const { hadRuntimeError, dependencyChain } = parsedData
              if (hadRuntimeError) {
                Log.warn(FAST_REFRESH_RUNTIME_RELOAD)
              }
              if (
                Array.isArray(dependencyChain) &&
                typeof dependencyChain[0] === 'string'
              ) {
                const cleanedModulePath = dependencyChain[0]
                  .replace(/^\[project\]/, '.')
                  .replace(/ \[.*\] \(.*\)$/, '')
                Log.warn(
                  `Fast Refresh had to perform a full reload when ${cleanedModulePath} changed. Read more: https://nextjs.org/docs/messages/fast-refresh-reload`
                )
              }
              break
            case 'client-added-page':
              // TODO
              break
            case 'browser-logs': {
              await receiveBrowserLogsTurbopack({
                entries: parsedData.entries,
                router: parsedData.router,
                sourceType: parsedData.sourceType,
                project,
                projectPath,
                distDir,
                config:
                  (nextConfig.logging &&
                    nextConfig.logging.browserToTerminal) ||
                  false,
              })
              break
            }
            case 'ping': {
              // Handle ping events to keep WebSocket connections alive
              // No-op - just acknowledge the ping
              break
            }

            case 'mcp-error-state-response': {
              handleErrorStateResponse(
                parsedData.requestId,
                parsedData.errorState,
                parsedData.url
              )
              break
            }

            case 'mcp-page-metadata-response': {
              handlePageMetadataResponse(
                parsedData.requestId,
                parsedData.segmentTrieData,
                parsedData.url
              )
              break
            }

            default:
              // Might be a Turbopack message...
              if (!parsedData.type) {
                throw new Error(`unrecognized HMR message "${data}"`)
              }
          }

          // Turbopack messages
          switch (parsedData.type) {
            case 'turbopack-subscribe':
              subscribeToClientHmrEvents(client, parsedData.path)
              break

            case 'turbopack-unsubscribe':
              unsubscribeFromClientHmrEvents(client, parsedData.path)
              break

            default:
              if (!parsedData.event) {
                throw new Error(`unrecognized Turbopack HMR message "${data}"`)
              }
          }
        })

        const turbopackConnectedMessage: TurbopackConnectedMessage = {
          type: HMR_MESSAGE_SENT_TO_BROWSER.TURBOPACK_CONNECTED,
          data: { sessionId },
        }
        sendToClient(client, turbopackConnectedMessage)

        const errors: CompilationError[] = []

        for (const entryIssues of currentEntryIssues.values()) {
          for (const issue of entryIssues.values()) {
            if (issue.severity !== 'warning') {
              errors.push({
                message: formatIssue(issue),
              })
            } else {
              printNonFatalIssue(issue)
            }
          }
        }

        if (devIndicatorServerState.disabledUntil < Date.now()) {
          devIndicatorServerState.disabledUntil = 0
        }

        ;(async function () {
          const versionInfo = await getVersionInfoCached()
          const devToolsConfig = await getDevToolsConfig(distDir)

          const syncMessage: SyncMessage = {
            type: HMR_MESSAGE_SENT_TO_BROWSER.SYNC,
            errors,
            warnings: [],
            hash: '',
            versionInfo,
            debug: {
              devtoolsFrontendUrl,
            },
            devIndicator: devIndicatorServerState,
            devToolsConfig,
          }

          sendToClient(client, syncMessage)
        })()
      })
    },

    send(action) {
      const payload = JSON.stringify(action)

      for (const client of [
        ...clientsWithoutHtmlRequestId,
        ...clientsByHtmlRequestId.values(),
      ]) {
        client.send(payload)
      }
    },

    sendToLegacyClients(action) {
      const payload = JSON.stringify(action)

      // Clients with a request ID are inferred App Router clients. If Cache
      // Components is not enabled, we consider those legacy clients. Pages
      // Router clients are also considered legacy clients. TODO: Maybe mark
      // clients as App Router / Pages Router clients explicitly, instead of
      // inferring it from the presence of a request ID.

      if (!nextConfig.cacheComponents) {
        for (const client of clientsByHtmlRequestId.values()) {
          client.send(payload)
        }
      }

      for (const client of clientsWithoutHtmlRequestId) {
        client.send(payload)
      }
    },

    setCacheStatus(status: ServerCacheStatus, htmlRequestId: string): void {
      // Legacy clients don't have Cache Components.
      const client = clientsByHtmlRequestId.get(htmlRequestId)
      if (client !== undefined) {
        sendToClient(client, {
          type: HMR_MESSAGE_SENT_TO_BROWSER.CACHE_INDICATOR,
          state: status,
        })
      } else {
        // If the client is not connected, store the status so that we can send it
        // when the client connects.
        cacheStatusesByHtmlRequestId.set(htmlRequestId, status)
      }
    },

    setReactDebugChannel(debugChannel, htmlRequestId, requestId) {
      const client = clientsByHtmlRequestId.get(htmlRequestId)

      if (htmlRequestId === requestId) {
        // The debug channel is for the HTML request.
        if (client) {
          // If the client is connected, we can connect the debug channel for
          // the HTML request immediately.
          connectReactDebugChannel(
            htmlRequestId,
            debugChannel,
            sendToClient.bind(null, client)
          )
        } else {
          // Otherwise, we'll do that when the client connects and just store
          // the debug channel.
          setReactDebugChannelForHtmlRequest(htmlRequestId, debugChannel)
        }
      } else if (client) {
        // The debug channel is for a subsequent request (e.g. client-side
        // navigation for server function call). If the client is not connected
        // anymore, we don't need to connect the debug channel.
        connectReactDebugChannel(
          requestId,
          debugChannel,
          sendToClient.bind(null, client)
        )
      }
    },

    sendErrorsToBrowser(errorsRscStream, htmlRequestId) {
      const client = clientsByHtmlRequestId.get(htmlRequestId)

      if (client) {
        // If the client is connected, we can send the errors immediately.
        sendSerializedErrorsToClient(
          errorsRscStream,
          sendToClient.bind(null, client)
        )
      } else {
        // Otherwise, store the errors stream so that we can send it when the
        // client connects.
        setErrorsRscStreamForHtmlRequest(htmlRequestId, errorsRscStream)
      }
    },

    setHmrServerError(_error) {
      // Not implemented yet.
    },
    clearHmrServerError() {
      // Not implemented yet.
    },
    async start() {},
    async getCompilationErrors(page) {
      const appEntryKey = getEntryKey('app', 'server', page)
      const pagesEntryKey = getEntryKey('pages', 'server', page)

      const topLevelIssues = currentTopLevelIssues.values()

      const thisEntryIssues =
        currentEntryIssues.get(appEntryKey) ??
        currentEntryIssues.get(pagesEntryKey)

      if (thisEntryIssues !== undefined && thisEntryIssues.size > 0) {
        // If there is an error related to the requesting page we display it instead of the first error
        return [...topLevelIssues, ...thisEntryIssues.values()]
          .map((issue) => {
            const formattedIssue = formatIssue(issue)
            if (issue.severity === 'warning') {
              printNonFatalIssue(issue)
              return null
            } else if (isWellKnownError(issue)) {
              Log.error(formattedIssue)
            }

            return new Error(formattedIssue)
          })
          .filter((error) => error !== null)
      }

      // Otherwise, return all errors across pages
      const errors = []
      for (const issue of topLevelIssues) {
        if (issue.severity !== 'warning') {
          errors.push(new Error(formatIssue(issue)))
        }
      }
      for (const entryIssues of currentEntryIssues.values()) {
        for (const issue of entryIssues.values()) {
          if (issue.severity !== 'warning') {
            const message = formatIssue(issue)
            errors.push(new Error(message))
          } else {
            printNonFatalIssue(issue)
          }
        }
      }
      return errors
    },
    async invalidate({
      // .env files or tsconfig/jsconfig change
      reloadAfterInvalidation,
    }) {
      if (reloadAfterInvalidation) {
        for (const [key, entrypoint] of currentWrittenEntrypoints) {
          clearRequireCache(key, entrypoint, { force: true })
        }

        await clearAllModuleContexts()
        this.send({
          type: HMR_MESSAGE_SENT_TO_BROWSER.SERVER_COMPONENT_CHANGES,
          hash: String(++hmrHash),
        })
      }
    },
    async buildFallbackError() {
      // Not implemented yet.
    },
    async ensurePage({
      page: inputPage,
      // Unused parameters
      // clientOnly,
      appPaths,
      definition,
      isApp,
      url: requestUrl,
    }) {
      // When there is no route definition this is an internal file not a route the user added.
      // Middleware and instrumentation are handled in turbpack-utils.ts handleEntrypoints instead.
      if (!definition) {
        if (inputPage === '/middleware') return
        if (inputPage === '/src/middleware') return
        if (inputPage === '/instrumentation') return
        if (inputPage === '/src/instrumentation') return
      }

      return hotReloaderSpan
        .traceChild('ensure-page', {
          inputPage,
        })
        .traceAsyncFn(async () => {
          if (BLOCKED_PAGES.includes(inputPage) && inputPage !== '/_error') {
            return
          }

          await currentEntriesHandling

          // TODO We shouldn't look into the filesystem again. This should use the information from entrypoints
          let routeDef: Pick<
            RouteDefinition,
            'filename' | 'bundlePath' | 'page'
          > =
            definition ??
            (await findPagePathData(
              projectPath,
              inputPage,
              nextConfig.pageExtensions,
              opts.pagesDir,
              opts.appDir,
              !!nextConfig.experimental.globalNotFound
            ))

          // If the route is actually an app page route, then we should have access
          // to the app route definition, and therefore, the appPaths from it.
          if (!appPaths && definition && isAppPageRouteDefinition(definition)) {
            appPaths = definition.appPaths
          }

          // Check if this is a deferred entry and wait for non-deferred entries first
          if (hasDeferredEntriesConfig) {
            const isDeferred = isDeferredEntry(
              routeDef.page,
              deferredEntriesConfig
            )
            if (isDeferred) {
              await processDeferredEntry()
            } else {
              // Track non-deferred entry as building
              nonDeferredBuildingEntries.add(routeDef.page)
            }
          }

          let page = routeDef.page
          if (appPaths) {
            const normalizedPage = normalizeAppPath(page)

            // filter out paths that are not exact matches (e.g. catchall)
            const matchingAppPaths = appPaths.filter(
              (path) => normalizeAppPath(path) === normalizedPage
            )

            // the last item in the array is the root page, if there are parallel routes
            page = matchingAppPaths[matchingAppPaths.length - 1]
          }

          const pathname = definition?.pathname ?? inputPage

          if (page === '/_error') {
            let finishBuilding = startBuilding(pathname, requestUrl, false)
            try {
              await handlePagesErrorRoute({
                currentEntryIssues,
                entrypoints: currentEntrypoints,
                manifestLoader,
                devRewrites: opts.fsChecker.rewrites,
                productionRewrites: undefined,
                logErrors: true,
                hooks: {
                  subscribeToChanges: subscribeToClientChanges,
                  handleWrittenEndpoint: (id, result, forceDeleteCache) => {
                    currentWrittenEntrypoints.set(id, result)
                    assetMapper.setPathsForKey(id, result.clientPaths)
                    return clearRequireCache(id, result, {
                      force: forceDeleteCache,
                    })
                  },
                },
              })
            } finally {
              finishBuilding()
            }
            return
          }

          const isInsideAppDir = routeDef.bundlePath.startsWith('app/')
          const isEntryMetadataRouteFile = isMetadataRouteFile(
            routeDef.filename.replace(opts.appDir || '', ''),
            nextConfig.pageExtensions,
            true
          )
          const normalizedAppPage = isEntryMetadataRouteFile
            ? normalizedPageToTurbopackStructureRoute(
                page,
                extname(routeDef.filename)
              )
            : page

          const route = isInsideAppDir
            ? currentEntrypoints.app.get(normalizedAppPage)
            : currentEntrypoints.page.get(page)

          if (!route) {
            // TODO: why is this entry missing in turbopack?
            if (page === '/middleware') return
            if (page === '/src/middleware') return
            if (page === '/proxy') return
            if (page === '/src/proxy') return
            if (page === '/instrumentation') return
            if (page === '/src/instrumentation') return

            throw new PageNotFoundError(`route not found ${page}`)
          }

          // We don't throw on ensureOpts.isApp === true for page-api
          // since this can happen when app pages make
          // api requests to page API routes.
          if (isApp && route.type === 'page') {
            throw new Error(`mis-matched route type: isApp && page for ${page}`)
          }

          const finishBuilding = startBuilding(pathname, requestUrl, false)
          try {
            await handleRouteType({
              dev,
              page,
              pathname,
              route,
              currentEntryIssues,
              entrypoints: currentEntrypoints,
              manifestLoader,
              readyIds,
              devRewrites: opts.fsChecker.rewrites,
              productionRewrites: undefined,
              logErrors: true,

              hooks: {
                subscribeToChanges: subscribeToClientChanges,
                handleWrittenEndpoint: (id, result, forceDeleteCache) => {
                  currentWrittenEntrypoints.set(id, result)
                  assetMapper.setPathsForKey(id, result.clientPaths)
                  return clearRequireCache(id, result, {
                    force: forceDeleteCache,
                  })
                },
              },
            })
          } finally {
            finishBuilding()
            // Remove non-deferred entry from building set
            if (hasDeferredEntriesConfig) {
              nonDeferredBuildingEntries.delete(routeDef.page)
            }
          }
        })
    },
    close() {
      // Report MCP telemetry if MCP server is enabled
      recordMcpTelemetry(opts.telemetry)

      for (const wsClient of [
        ...clientsWithoutHtmlRequestId,
        ...clientsByHtmlRequestId.values(),
      ]) {
        // it's okay to not cleanly close these websocket connections, this is dev
        wsClient.terminate()
      }
      clientsWithoutHtmlRequestId.clear()
      clientsByHtmlRequestId.clear()
    },
  }

  handleEntrypointsSubscription().catch((err) => {
    console.error(err)
    process.exit(1)
  })

  // Write empty manifests
  await currentEntriesHandling
  await manifestLoader.writeManifests({
    devRewrites: opts.fsChecker.rewrites,
    productionRewrites: undefined,
    entrypoints: currentEntrypoints,
  })

  async function handleProjectUpdates() {
    for await (const updateMessage of project.updateInfoSubscribe(30)) {
      switch (updateMessage.updateType) {
        case 'start': {
          hotReloader.send({ type: HMR_MESSAGE_SENT_TO_BROWSER.BUILDING })
          // Mark that HMR has started and we need to call the callback after it settles
          // This ensures onBeforeDeferredEntries will be called again during HMR
          if (hasDeferredEntriesConfig) {
            hmrPendingDeferredCallback = true
            onBeforeDeferredEntriesCalled = false
            onBeforeDeferredEntriesPromise = null
          }
          break
        }
        case 'end': {
          sendEnqueuedMessages()

          function addToErrorsMap(
            errorsMap: Map<string, CompilationError>,
            issueMap: IssuesMap
          ) {
            for (const [key, issue] of issueMap) {
              if (issue.severity === 'warning') continue
              if (errorsMap.has(key)) continue

              const message = formatIssue(issue)

              errorsMap.set(key, {
                message,
                details: issue.detail
                  ? renderStyledStringToErrorAnsi(issue.detail)
                  : undefined,
              })
            }
          }

          function addErrors(
            errorsMap: Map<string, CompilationError>,
            issues: EntryIssuesMap
          ) {
            for (const issueMap of issues.values()) {
              addToErrorsMap(errorsMap, issueMap)
            }
          }

          const errors = new Map<string, CompilationError>()
          addToErrorsMap(errors, currentTopLevelIssues)
          addErrors(errors, currentEntryIssues)

          for (const client of [
            ...clientsWithoutHtmlRequestId,
            ...clientsByHtmlRequestId.values(),
          ]) {
            const state = clientStates.get(client)
            if (!state) {
              continue
            }

            const clientErrors = new Map(errors)
            addErrors(clientErrors, state.clientIssues)

            sendToClient(client, {
              type: HMR_MESSAGE_SENT_TO_BROWSER.BUILT,
              hash: String(++hmrHash),
              errors: [...clientErrors.values()],
              warnings: [],
            })
          }

          if (hmrEventHappened) {
            const time = updateMessage.value.duration
            const timeMessage =
              time > 2000 ? `${Math.round(time / 100) / 10}s` : `${time}ms`
            Log.event(`Compiled in ${timeMessage}`)
            hmrEventHappened = false
          }

          // Call onBeforeDeferredEntries after compilation completes during HMR
          // This ensures the callback is invoked even when non-deferred entries change
          // Use debounced function to prevent rapid-fire calls from turbopack updates
          if (hasDeferredEntriesConfig) {
            callOnBeforeDeferredEntriesAfterHMR()
          }
          break
        }
        default:
      }
    }
  }

  handleProjectUpdates().catch((err) => {
    console.error(err)
    process.exit(1)
  })

  if (serverFastRefresh) {
    serverHmrSubscriptions = setupServerHmr(project, {
      clear: async () => {
        // Clear Node's require cache of all Turbopack-built modules
        const chunkPaths = [...(serverHmrSubscriptions?.keys() ?? [])].map(
          (chunkPath) => join(distDir, chunkPath)
        )
        deleteCache(chunkPaths)

        // Clear Turbopack's runtime caches
        if (typeof __next__clear_chunk_cache__ === 'function') {
          __next__clear_chunk_cache__()
        }

        // Reset the server HMR handler registry. All server runtime chunks are
        // cleared from require.cache above; when they're next required they'll
        // re-register into this Map and reinstall the routing dispatcher.
        ;(globalThis as any).__turbopack_server_hmr_handlers__ = new Map()

        // Clear all edge contexts
        await clearAllModuleContexts()

        resetFetch()

        // Tell browsers to refetch RSC (soft refresh, not full page reload)
        hotReloader.send({
          type: HMR_MESSAGE_SENT_TO_BROWSER.SERVER_COMPONENT_CHANGES,
          hash: String(++hmrHash),
        })
      },
    })
  }

  return hotReloader
}
Quest for Codev2.0.0
/
SIGN IN