Skip to content
Merged
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
20 changes: 11 additions & 9 deletions packages/nuxt-cli/src/commands/dev.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import type { ParsedArgs } from 'citty'
import type { HTTPSOptions } from '../dev/cert'
import type { DevListenOverrides } from '../dev/listen'
import type { DevRestartReason } from '../dev/reason'
import type { NuxtDevContext } from '../dev/utils'

import process from 'node:process'

import { defineCommand } from 'citty'

import colors from 'picocolors'
import { isBun, isTest } from 'std-env'
import { satisfies } from 'verkit'
import { initialize } from '../dev'

import { closeInspector, openInspector, resolveInspectOptions } from '../dev/inspect'
import { ForkPool } from '../dev/pool'
import { formatRestartReason } from '../dev/reason'
import { setupShortcuts } from '../dev/shortcuts'
import { debug, logger } from '../utils/logger'
import { resolveRootDir } from '../utils/paths'
Expand Down Expand Up @@ -161,7 +162,7 @@ const command = defineCommand({

// Disable forking when profiling to capture all activity in one process
if (!ctx.args.fork || ctx.args.profile) {
setupShortcuts({ listener, close, onReady, restart: () => reload('Restart requested') })
setupShortcuts({ listener, close, onReady, restart: () => reload({ type: 'shortcut' }) })
setupSignalHandlers(close)
return {
listener,
Expand Down Expand Up @@ -191,7 +192,9 @@ const command = defineCommand({
// On hard restart, use a fork from the pool
let cleanupCurrentFork: (() => Promise<void>) | undefined

async function restartWithFork() {
async function restartWithFork(reason?: DevRestartReason) {
logger.info(formatRestartReason(reason, { rootDir: cwd, hard: true }))

// Get a fork from the pool (warm if available, cold otherwise)
const context: NuxtDevContext = { cwd, args: ctx.args }

Expand All @@ -210,19 +213,18 @@ const command = defineCommand({
}
else if (message.type === 'nuxt:internal:dev:restart') {
// Fork is requesting another restart
void restartWithFork()
void restartWithFork(message.reason)
}
else if (message.type === 'nuxt:internal:dev:rejection') {
logger.info(`Restarting Nuxt due to error: ${colors.cyan(message.message)}`)
void restartWithFork()
void restartWithFork({ type: 'error', message: message.message })
}
})
}

async function restart() {
async function restart(reason?: DevRestartReason) {
// Close the in-process dev server
await close()
await restartWithFork()
await restartWithFork(reason)
}

onRestart(restart)
Expand All @@ -232,7 +234,7 @@ const command = defineCommand({
await close()
}

setupShortcuts({ listener, close: closeAll, onReady, restart })
setupShortcuts({ listener, close: closeAll, onReady, restart: () => restart({ type: 'shortcut' }) })
setupSignalHandlers(closeAll)

return {
Expand Down
25 changes: 15 additions & 10 deletions packages/nuxt-cli/src/dev/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { NuxtConfig } from '@nuxt/schema'
import type { DevListenOverrides, Listener } from './listen'
import type { DevRestartReason } from './reason'
import type { NuxtDevContext, NuxtDevIPCMessage, NuxtParentIPCMessage } from './utils'

import process from 'node:process'
Expand All @@ -13,6 +14,10 @@ import { NuxtDevServer } from './utils'

const start = Date.now()

function formatErrorMessage(error: unknown): string {
return error instanceof Error ? error.toString() : 'Unhandled Rejection'
}

interface InitializeOptions {
data?: {
overrides?: NuxtConfig
Expand All @@ -33,7 +38,7 @@ class IPC {
process.exit(0)
})
process.once('unhandledRejection', (reason) => {
this.send({ type: 'nuxt:internal:dev:rejection', message: reason instanceof Error ? reason.toString() : 'Unhandled Rejection' })
this.send({ type: 'nuxt:internal:dev:rejection', message: formatErrorMessage(reason) })
process.exit()
})
}
Expand Down Expand Up @@ -61,11 +66,11 @@ interface InitializeReturn {
listener: Listener
close: () => Promise<void>
/** Reload Nuxt in place, keeping the current listener. */
reload: (reason?: string) => Promise<void>
reload: (reason?: DevRestartReason) => Promise<void>
onReady: (callback: (address: string) => void) => void
/** Called the first time a watched file changes, before Nuxt reloads. */
onFileChange: (callback: () => void) => void
onRestart: (callback: (devServer: NuxtDevServer) => void) => void
onRestart: (callback: (reason?: DevRestartReason) => void) => void
}

export async function initialize(devContext: NuxtDevContext, ctx: InitializeOptions = {}): Promise<InitializeReturn> {
Expand Down Expand Up @@ -113,8 +118,8 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti
devServer.on('loading', (message) => {
ipc.send({ type: 'nuxt:internal:dev:loading', message })
})
devServer.on('restart', () => {
ipc.send({ type: 'nuxt:internal:dev:restart' })
devServer.on('restart', (reason) => {
ipc.send({ type: 'nuxt:internal:dev:restart', reason })
})
devServer.on('ready', (payload) => {
ipc.send({ type: 'nuxt:internal:dev:ready', address: payload })
Expand Down Expand Up @@ -149,7 +154,7 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti

return {
listener: devServer.listener,
reload: (reason?: string) => devServer.load(true, reason),
reload: (reason?: DevRestartReason) => devServer.load(true, reason),
close: () => {
closePromise ??= (async () => {
devServer.closeWatchers()
Expand All @@ -176,22 +181,22 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti
onFileChange: (callback: () => void) => {
devServer.once('change', callback)
},
onRestart: (callback: (devServer: NuxtDevServer) => void) => {
onRestart: (callback: (reason?: DevRestartReason) => void) => {
let restarted = false
function restart() {
function restart(reason?: DevRestartReason) {
if (!restarted) {
restarted = true
process.off('uncaughtException', restartOnError)
process.off('unhandledRejection', restartOnError)
callback(devServer)
callback(reason)
}
}
function restartOnError(error: unknown) {
if (isBrokenPipe(error)) {
debug('Ignoring broken pipe:', error)
return
}
restart()
restart({ type: 'error', message: formatErrorMessage(error) })
}
devServer.once('restart', restart)
process.on('uncaughtException', restartOnError)
Expand Down
75 changes: 75 additions & 0 deletions packages/nuxt-cli/src/dev/reason.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { relativeTo } from '../utils/paths'

/** Structured cause of a dev server reload or restart. */
export type DevRestartReason
= | { type: 'config', files: string[], keys?: string[] }
| { type: 'dist-removed' }
| { type: 'hook' }
| { type: 'shortcut' }
| { type: 'error', message: string }

export interface FormatRestartReasonOptions {
rootDir: string
/** A new process is taking over, rather than reloading in place. */
hard?: boolean
/** Render file names as terminal hyperlinks. Disable for non-terminal output. */
link?: boolean
}

/**
* Combine a queued reason with a newer one, so a debounced reload can report
* every file that changed within the window.
*/
export function mergeRestartReasons(previous: DevRestartReason | undefined, next: DevRestartReason): DevRestartReason {
if (previous?.type !== 'config' || next.type !== 'config') {
return next
}
return {
type: 'config',
files: [...new Set([...previous.files, ...next.files])],
keys: previous.keys || next.keys
? [...new Set([...previous.keys || [], ...next.keys || []])]
: undefined,
}
}

/** Describe what caused a reload or restart, without saying what happens next. */
export function formatRestartCause(reason: DevRestartReason, options: FormatRestartReasonOptions): string {
switch (reason.type) {
case 'config': {
const keys = reason.keys?.length ? ` (${reason.keys.join(', ')})` : ''
return `${formatFileList(reason.files, options)} changed${keys}`
}
case 'dist-removed':
return 'Build output was removed'
case 'hook':
return 'Nuxt requested a restart'
case 'shortcut':
return 'Restart requested'
case 'error':
return `Unhandled error: ${reason.message}`
}
}

/** One line saying what caused a reload or restart and what is happening as a result. */
export function formatRestartReason(reason: DevRestartReason | undefined, options: FormatRestartReasonOptions): string {
const action = options.hard ? 'Restarting Nuxt in a new process' : 'Reloading Nuxt'
if (!reason) {
return `${action}...`
}
return `${formatRestartCause(reason, options)}. ${action}...`
Comment on lines +56 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore the in-place action text.

This returns Reloading Nuxt..., while the new tests expect Reloading Nuxt in place...; both assertions will fail.

Proposed fix
-  const action = options.hard ? 'Restarting Nuxt in a new process' : 'Reloading Nuxt'
+  const action = options.hard ? 'Restarting Nuxt in a new process' : 'Reloading Nuxt in place'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const action = options.hard ? 'Restarting Nuxt in a new process' : 'Reloading Nuxt'
if (!reason) {
return `${action}...`
}
return `${formatRestartCause(reason, options)}. ${action}...`
const action = options.hard ? 'Restarting Nuxt in a new process' : 'Reloading Nuxt in place'
if (!reason) {
return `${action}...`
}
return `${formatRestartCause(reason, options)}. ${action}...`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nuxt-cli/src/dev/reason.ts` around lines 56 - 60, Update the action
text in the reason-formatting logic to return “Reloading Nuxt in place...” for
non-hard restarts, while preserving “Restarting Nuxt in a new process” for hard
restarts and the existing formatRestartCause handling.

}

const MAX_LISTED_FILES = 2

function formatFileList(files: string[], options: FormatRestartReasonOptions): string {
const names = files.map(file => options.link === false ? relativeTo(options.rootDir, file, { link: false }) : relativeTo(options.rootDir, file))
if (names.length === 0) {
return 'A file'
}
if (names.length <= MAX_LISTED_FILES) {
return names.join(' and ')
}
const remaining = names.length - MAX_LISTED_FILES
return `${names.slice(0, MAX_LISTED_FILES).join(', ')} and ${remaining} other file${remaining === 1 ? '' : 's'}`
}
Loading
Loading