Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 2 additions & 26 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ import path from 'path'
import { promises as fs } from 'fs'
import { isValidElementType } from 'next/dist/compiled/react-is'
import stripAnsi from 'next/dist/compiled/strip-ansi'
import browserslist from 'next/dist/compiled/browserslist'
import {
MODERN_BROWSERSLIST_TARGET,
UNDERSCORE_GLOBAL_ERROR_ROUTE,
UNDERSCORE_GLOBAL_ERROR_ROUTE_ENTRY,
UNDERSCORE_NOT_FOUND_ROUTE,
Expand Down Expand Up @@ -1528,30 +1526,8 @@ export class NestedMiddlewareError extends Error {
}
}

export function getSupportedBrowsers(
dir: string,
isDevelopment: boolean
): string[] {
let browsers: any
try {
const browsersListConfig = browserslist.loadConfig({
path: dir,
env: isDevelopment ? 'development' : 'production',
})
// Running `browserslist` resolves `extends` and other config features into a list of browsers
if (browsersListConfig && browsersListConfig.length > 0) {
browsers = browserslist(browsersListConfig)
}
} catch {}

// When user has browserslist use that target
if (browsers && browsers.length > 0) {
return browsers
}

// Uses modern browsers as the default.
return MODERN_BROWSERSLIST_TARGET
}
// Re-export from lightweight module for backwards compatibility
export { getSupportedBrowsers } from './get-supported-browsers'

// Re-export webpack layer utilities from dedicated module (for backwards compatibility)
export {
Expand Down
22 changes: 13 additions & 9 deletions packages/next/src/server/dev/hot-reloader-turbopack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import {
type SetupOpts,
} from '../lib/router-utils/setup-dev-bundler'
import { TurbopackManifestLoader } from '../../shared/lib/turbopack/manifest-loader'
import { findPagePathData } from './on-demand-entry-handler'
// findPagePathData is lazy loaded - on-demand-entry-handler imports build/entries which is heavy
import type { RouteDefinition } from '../route-definitions/route-definition'
import {
type EntryKey,
Expand Down Expand Up @@ -97,7 +97,7 @@ 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, printBuildErrors } from '../../build/utils'
import { getSupportedBrowsers } from '../../build/get-supported-browsers'
import {
receiveBrowserLogsTurbopack,
handleClientFileLogs,
Expand Down Expand Up @@ -653,6 +653,8 @@ export async function createHotReloaderTurbopack(

// Certain crtical issues prevent any entrypoints from being constructed so return early
if (!('routes' in entrypoints)) {
const { printBuildErrors } =
require('../../build/utils') as typeof import('../../build/utils')
printBuildErrors(entrypoints, true)

currentEntriesHandlingResolve!()
Expand Down Expand Up @@ -1312,19 +1314,21 @@ export async function createHotReloaderTurbopack(
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(
let routeDef:
| Pick<RouteDefinition, 'filename' | 'bundlePath' | 'page'>
| undefined = definition
if (!routeDef) {
const { findPagePathData } =
require('./on-demand-entry-handler') as typeof import('./on-demand-entry-handler')
routeDef = 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.
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/server/lib/app-info-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export async function getStartServerInfo({
envInfo?: string[]
experimentalFeatures?: ConfiguredExperimentalFeature[]
cacheComponents?: boolean
logging?: boolean
}> {
let experimentalFeatures: ConfiguredExperimentalFeature[] = []
let cacheComponents = false
Expand Down Expand Up @@ -146,5 +147,6 @@ export async function getStartServerInfo({
envInfo,
experimentalFeatures,
cacheComponents,
logging: config.logging !== false,
}
}
37 changes: 23 additions & 14 deletions packages/next/src/server/lib/start-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,12 +357,20 @@ export async function startServer(
let envInfo: string[] | undefined
let experimentalFeatures: ConfiguredExperimentalFeature[] | undefined
let cacheComponents: boolean | undefined
let logging: boolean | undefined
try {
if (isDev) {
const startServerInfo = await getStartServerInfo({ dir, dev: isDev })
envInfo = startServerInfo.envInfo
cacheComponents = startServerInfo.cacheComponents
experimentalFeatures = startServerInfo.experimentalFeatures
logging = startServerInfo.logging

// Set logging state before any output to respect next.config.js setting
if (logging !== undefined) {
const { store: consoleStore } = (require('../../build/output/store') as typeof import('../../build/output/store'))
consoleStore.setState({ logging })
}
}
logStartInfo({
networkUrl,
Expand All @@ -373,6 +381,21 @@ export async function startServer(
logBundler: isDev,
})

// Print "Ready" immediately for fast perceived startup
// Requests will wait via handlersPromise until init completes
const startServerProcessDuration =
performance.mark('next-start-end') &&
performance.measure(
'next-start-duration',
'next-start',
'next-start-end'
).duration
const formatDurationText =
startServerProcessDuration > 2000
? `${Math.round(startServerProcessDuration / 100) / 10}s`
: `${Math.round(startServerProcessDuration)}ms`
Log.event(`Ready in ${formatDurationText}`)
Comment on lines +384 to +397
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Ready in Xms" message is printed before the heavy initialization completes. If initialization fails (at line 457-469), the misleading "Ready" message has already been logged, but the server exits with an error.

View Details
📝 Patch Details
diff --git a/packages/next/src/server/lib/start-server.ts b/packages/next/src/server/lib/start-server.ts
index ea219758a9..169b46a716 100644
--- a/packages/next/src/server/lib/start-server.ts
+++ b/packages/next/src/server/lib/start-server.ts
@@ -373,21 +373,6 @@ export async function startServer(
           logBundler: isDev,
         })
 
-        // Print "Ready" immediately for fast perceived startup
-        // Requests will wait via handlersPromise until init completes
-        const startServerProcessDuration =
-          performance.mark('next-start-end') &&
-          performance.measure(
-            'next-start-duration',
-            'next-start',
-            'next-start-end'
-          ).duration
-        const formatDurationText =
-          startServerProcessDuration > 2000
-            ? `${Math.round(startServerProcessDuration / 100) / 10}s`
-            : `${Math.round(startServerProcessDuration)}ms`
-        Log.event(`Ready in ${formatDurationText}`)
-
         let cleanupStarted = false
         let closeUpgraded: (() => void) | null = null
         const cleanup = () => {
@@ -474,6 +459,20 @@ export async function startServer(
 
         handlersReady()
 
+        // Print "Ready" only after successful initialization
+        const startServerProcessDuration =
+          performance.mark('next-start-end') &&
+          performance.measure(
+            'next-start-duration',
+            'next-start',
+            'next-start-end'
+          ).duration
+        const formatDurationText =
+          startServerProcessDuration > 2000
+            ? `${Math.round(startServerProcessDuration / 100) / 10}s`
+            : `${Math.round(startServerProcessDuration)}ms`
+        Log.event(`Ready in ${formatDurationText}`)
+
         if (process.env.TURBOPACK && isDev) {
           await validateTurboNextConfig({
             dir: serverOptions.dir,

Analysis

Misleading "Ready in Xms" message logged before critical initialization completes

What fails: Server prints "Ready in Xms" message at line 389 before calling getRequestHandlers() at line 457. If initialization fails, the "Ready" message has already been logged to the user's console, but then the server exits with an error.

How to reproduce:

  1. Create a configuration scenario that causes getRequestHandlers() to throw an error (e.g., corrupted config, missing dependencies during initialization)
  2. Start the Next.js dev server
  3. Observe that "Ready in Xms" is logged to console
  4. The server then exits with an error code and prints the error stack trace

Result: Users see misleading output suggesting the server started successfully when it actually failed during initialization.

Expected: The "Ready" message should only be logged after getRequestHandlers() completes successfully. If initialization fails, the "Ready" message should never be printed.

Root cause: Commit 3e57cf5 ("perf(dev): print Ready early and defer initialization") moved the "Ready" log message before the critical getRequestHandlers() initialization call as a performance optimization. While the defensive mechanism via handlersPromise prevents request processing until initialization completes, it does not prevent the misleading log message from being printed on failure.

Fix: Moved the "Ready in Xms" log message to after handlersReady() is called (line 461-475), ensuring it only prints after successful initialization. The log now correctly appears after all critical initialization in getRequestHandlers() completes.


let cleanupStarted = false
let closeUpgraded: (() => void) | null = null
const cleanup = () => {
Expand Down Expand Up @@ -457,21 +480,7 @@ export async function startServer(
nextServer = initResult.server
closeUpgraded = initResult.closeUpgraded

const startServerProcessDuration =
performance.mark('next-start-end') &&
performance.measure(
'next-start-duration',
'next-start',
'next-start-end'
).duration

handlersReady()
const formatDurationText =
startServerProcessDuration > 2000
? `${Math.round(startServerProcessDuration / 100) / 10}s`
: `${Math.round(startServerProcessDuration)}ms`

Log.event(`Ready in ${formatDurationText}`)

if (process.env.TURBOPACK && isDev) {
await validateTurboNextConfig({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ describe('isolated-dev-build', () => {
})

it('should create dev artifacts in .next/dev/ directory', async () => {
// Make a request to trigger compilation and directory creation
const browser = await next.browser('/')
await browser.close()

expect(await next.hasFile('.next/dev')).toBe(true)
expect(await next.hasFile('.next/server')).toBe(false)
})
Expand Down
6 changes: 5 additions & 1 deletion test/development/app-dir/isolated-dev-build/next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}
const nextConfig = {
experimental: {
isolatedDevBuild: true,
},
}

module.exports = nextConfig
40 changes: 36 additions & 4 deletions test/integration/invalid-custom-routes/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

import fs from 'fs-extra'
import { join } from 'path'
import { launchApp, findPort, nextBuild } from 'next-test-utils'
import {
launchApp,
findPort,
nextBuild,
killApp,
fetchViaHTTP,
retry,
} from 'next-test-utils'

let appDir = join(__dirname, '..')
const nextConfigPath = join(appDir, 'next.config.js')
Expand Down Expand Up @@ -582,18 +589,43 @@ describe('Errors on invalid custom routes', () => {
'development mode',
() => {
let stderr = ''
let app
let port
beforeAll(() => {
getStderr = async () => {
const port = await findPort()
await launchApp(appDir, port, {
stderr = ''
port = await findPort()
app = await launchApp(appDir, port, {
onStderr: (msg) => {
stderr += msg
},
})
// Make a request to trigger route validation (lazy loaded with early-ready optimization)
try {
await fetchViaHTTP(port, '/')
} catch {
// ignore - request may fail due to invalid config
}
// Wait for stderr to be populated
await retry(
() => {
if (stderr.length === 0) {
throw new Error('stderr still empty')
}
},
3000,
500
).catch(() => {
// If no stderr after waiting, that's okay - test will fail with assertion
})
return stderr
}
})
afterEach(() => {
afterEach(async () => {
if (app) {
await killApp(app)
app = undefined
}
stderr = ''
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ describe('TypeScript Image Component', () => {
afterAll(() => killApp(app))

it('should have image types when enabled', async () => {
// Make a request to ensure full initialization is complete
await renderViaHTTP(appPort, '/')
const envTypes = await fs.readFile(
join(appDir, 'next-env.d.ts'),
'utf8'
Expand Down Expand Up @@ -96,7 +98,10 @@ describe('TypeScript Image Component', () => {
nextConfig,
content.replace('// disableStaticImages', 'disableStaticImages')
)
const app = await launchApp(appDir, await findPort())
const port = await findPort()
const app = await launchApp(appDir, port)
// Make a request to ensure full initialization is complete
await renderViaHTTP(port, '/')
await killApp(app)
await fs.writeFile(nextConfig, content)
const envTypes = await fs.readFile(join(appDir, 'next-env.d.ts'), 'utf8')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ describe('TypeScript Image Component', () => {
afterAll(() => killApp(app))

it('should have image types when enabled', async () => {
// Make a request to ensure full initialization is complete
await renderViaHTTP(appPort, '/')
const envTypes = await fs.readFile(
join(appDir, 'next-env.d.ts'),
'utf8'
Expand All @@ -95,7 +97,10 @@ describe('TypeScript Image Component', () => {
nextConfig,
content.replace('// disableStaticImages', 'disableStaticImages')
)
const app = await launchApp(appDir, await findPort())
const port = await findPort()
const app = await launchApp(appDir, port)
// Make a request to ensure full initialization is complete
await renderViaHTTP(port, '/')
await killApp(app)
await fs.writeFile(nextConfig, content)
const envTypes = await fs.readFile(join(appDir, 'next-env.d.ts'), 'utf8')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-env jest */

import { join } from 'path'
import { findPort, launchApp, killApp } from 'next-test-utils'
import { findPort, launchApp, killApp, renderViaHTTP } from 'next-test-utils'
import { promises as fs } from 'fs'

const appDir = join(__dirname, '..')
Expand All @@ -16,6 +16,8 @@ describe('TypeScript App Type Declarations', () => {
let app
try {
app = await launchApp(appDir, appPort, {})
// Make a request to ensure full initialization is complete
await renderViaHTTP(appPort, '/')
const content = await fs.readFile(appTypeDeclarations, 'utf8')
expect(content).toEqual(prevContent)
} finally {
Expand All @@ -34,6 +36,8 @@ describe('TypeScript App Type Declarations', () => {
let app
try {
app = await launchApp(appDir, appPort, {})
// Make a request to ensure full initialization is complete
await renderViaHTTP(appPort, '/')
const content = await fs.readFile(appTypeDeclarations, 'utf8')
expect(content).toEqual(prevContent)
} finally {
Expand All @@ -50,6 +54,8 @@ describe('TypeScript App Type Declarations', () => {
let app
try {
app = await launchApp(appDir, appPort, {})
// Make a request to ensure full initialization is complete
await renderViaHTTP(appPort, '/')
const stat = await fs.stat(appTypeDeclarations)
expect(stat.mtime).toEqual(prevStat.mtime)
} finally {
Expand Down
Loading