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
}