From 728d2b59f5d57b61059bf9134a250bf4ca6ac186 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 24 Aug 2026 10:23:41 +0000 Subject: [PATCH 1/7] feat(dev): narrate the first page render after the server is ready --- packages/nuxt-cli/src/dev/index.ts | 10 +- packages/nuxt-cli/src/dev/progress.ts | 101 ++++++++++- packages/nuxt-cli/src/dev/tui/session.ts | 11 +- packages/nuxt-cli/src/dev/utils.ts | 7 + packages/nuxt-cli/src/utils/phase-reporter.ts | 161 +++++++++++++----- .../nuxt-cli/src/utils/progress-snapshot.ts | 20 +++ .../nuxt-cli/test/unit/dev-progress.spec.ts | 56 +++++- packages/nuxt-cli/test/unit/dev-tui.spec.ts | 8 + .../nuxt-cli/test/unit/phase-reporter.spec.ts | 76 +++++++++ 9 files changed, 392 insertions(+), 58 deletions(-) diff --git a/packages/nuxt-cli/src/dev/index.ts b/packages/nuxt-cli/src/dev/index.ts index 3558078be..c85839d81 100644 --- a/packages/nuxt-cli/src/dev/index.ts +++ b/packages/nuxt-cli/src/dev/index.ts @@ -366,6 +366,11 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti ? undefined : createPhaseReporter({ heartbeat: STARTUP_HEARTBEAT_MS }) const unsubscribeProgress = reporter && devServer.progress.onUpdate(reporter.update) + if (reporter) { + // The URL block is printed as soon as the socket is bound, which on a large + // project is a screenful of build output above the summary. + devServer.on('listening', ({ url }) => reporter.setURL(url)) + } const stopReporting = () => { unsubscribeProgress?.() reporter?.stop() @@ -376,8 +381,9 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti } finally { // Nuxt being ready ends startup for this process but not for whoever is - // waiting on the first render, so the reporter stays subscribed. Any state - // other than ready-but-not-serving has nothing left to wait for. + // waiting on the first page, so the reporter stays subscribed to narrate + // it. Any state other than ready-but-not-serving has nothing left to wait + // for. const snapshot = devServer.progress.snapshot if (!reporter || snapshot?.status !== 'ready' || snapshot.serving) { stopReporting() diff --git a/packages/nuxt-cli/src/dev/progress.ts b/packages/nuxt-cli/src/dev/progress.ts index 8fe66e13f..bb51bea02 100644 --- a/packages/nuxt-cli/src/dev/progress.ts +++ b/packages/nuxt-cli/src/dev/progress.ts @@ -1,5 +1,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http' -import type { PhaseTiming, ProgressSnapshot, ProgressStatus } from '../utils/progress-snapshot' +import type { PendingRender, PhaseTiming, ProgressSnapshot, ProgressStatus } from '../utils/progress-snapshot' + +import { READY_MESSAGE } from '../utils/progress-snapshot' /** Path prefix reserved for the CLI's own dev-time endpoints. */ export const DEV_INTERNAL_PREFIX: string = '/__nuxt_dev__/' @@ -23,7 +25,7 @@ const DEV_PHASES: readonly DevPhase[] = [ { id: 'types', message: 'Generating types' }, { id: 'bundle', message: 'Bundling app' }, { id: 'server', message: 'Building server' }, - { id: 'ready', message: 'Ready' }, + { id: 'ready', message: READY_MESSAGE }, ] /** @@ -36,6 +38,13 @@ const READY_PROGRESS = 0.95 /** Shown between the server accepting requests and it answering one. */ const WARMUP_MESSAGE = 'Compiling the first request' +/** + * How long a request has to be in flight before it is reported. A page a dev + * server has already compiled is answered in milliseconds, and a status line + * that appears and disappears within a frame is noise. + */ +const REQUEST_DWELL = 400 + const HOOK_PHASES: Record = { 'modules:before': 'modules', 'builder:generateApp': 'app', @@ -198,6 +207,10 @@ export class DevProgress { #hooks: ActiveHook[] = [] #narrating = false #serving = false + #inflight = new Map() + #pending?: PendingRender + #dwell?: NodeJS.Timeout + #nextRequest = 0 #observing = false #installedModules?: () => number #observed = new WeakSet() @@ -218,6 +231,7 @@ export class DevProgress { phaseElapsed: Date.now() - this.#phaseStartedAt, reload: this.#reload, serving: this.#serving, + pending: this.#pending, timings: this.#timings, error: this.#error && { name: this.#error.name, message: this.#error.message }, } @@ -245,6 +259,7 @@ export class DevProgress { this.#timings = [] this.#reload = reload this.#serving = false + this.#clearPending() this.#startedAt = Date.now() this.#phaseStartedAt = this.#startedAt this.#baseMessage = message || DEV_PHASES[0]!.message @@ -272,23 +287,92 @@ export class DevProgress { this.#advance('ready', undefined, false) this.#status = 'ready' this.#error = undefined - // Nobody is watching a loading page, so there is no first render to wait - // for: whoever asks next pays for it, and the panel reports that request - // like any other. - this.#serving = this.#clients.size === 0 - if (!this.#serving) { + // Accepting a request is not answering one, so the wait is only over once a + // document has been rendered. Until then the message says which of the two + // has happened: something is already waiting for a page, or nothing is. + if (this.#clients.size > 0 || this.#pending || this.#inflight.size > 0) { this.#message = WARMUP_MESSAGE } this.#emit() } + /** + * Record a request the app is now rendering, returning the handle to settle + * it with. Reported only once it has been in flight for {@link REQUEST_DWELL}, + * so a page that is already compiled passes without comment. + */ + startRequest(label: string): number { + const id = ++this.#nextRequest + this.#inflight.set(id, { label, startedAt: Date.now() }) + this.#scheduleDwell() + return id + } + + /** Settle the request {@link startRequest} handed back. */ + finishRequest(id: number): void { + const request = this.#inflight.get(id) + if (!request) { + return + } + this.#inflight.delete(id) + if (this.#pending !== request) { + return + } + // Whatever else is still in flight has been waiting at least as long, so it + // takes over the line rather than leaving it blank. + this.#pending = this.#oldest(Date.now() - REQUEST_DWELL) + this.#scheduleDwell() + this.#emit() + } + + /** The oldest request in flight since before `since`, if any. */ + #oldest(since: number): PendingRender | undefined { + let oldest: PendingRender | undefined + for (const request of this.#inflight.values()) { + if (request.startedAt <= since && (!oldest || request.startedAt < oldest.startedAt)) { + oldest = request + } + } + return oldest + } + + /** Report the oldest request in flight once it has dwelt long enough. */ + #scheduleDwell(): void { + if (this.#dwell || this.#pending) { + return + } + let next: number | undefined + for (const { startedAt } of this.#inflight.values()) { + next = next === undefined ? startedAt : Math.min(next, startedAt) + } + if (next === undefined) { + return + } + this.#dwell = setTimeout(() => { + this.#dwell = undefined + this.#pending = this.#oldest(Date.now() - REQUEST_DWELL) + if (this.#pending) { + this.#emit() + } + this.#scheduleDwell() + }, Math.max(0, next + REQUEST_DWELL - Date.now())) + this.#dwell.unref?.() + } + + #clearPending(): void { + clearTimeout(this.#dwell) + this.#dwell = undefined + this.#pending = undefined + this.#inflight.clear() + } + /** The app has answered a request, so the wait is genuinely over. */ setServing(): void { if (this.#serving || this.#status !== 'ready') { return } this.#serving = true - this.#message = DEV_PHASES.at(-1)!.message + this.#message = READY_MESSAGE this.#emit() } @@ -548,6 +632,7 @@ export class DevProgress { close(): void { this.#stopNarrating() + this.#clearPending() clearInterval(this.#heartbeat) this.#heartbeat = undefined for (const client of this.#clients) { diff --git a/packages/nuxt-cli/src/dev/tui/session.ts b/packages/nuxt-cli/src/dev/tui/session.ts index 2e21be6b9..ac3b0e1b7 100644 --- a/packages/nuxt-cli/src/dev/tui/session.ts +++ b/packages/nuxt-cli/src/dev/tui/session.ts @@ -180,13 +180,16 @@ export function beginDevUI(options: DevUISupportOptions & { version?: string, cw if (snapshot.status === 'ready') { // Between the server accepting requests and answering one there is // nothing to watch but a badge, so it says which of the two has happened. - state.awaitingFirstRender = !snapshot.serving - state.note = snapshot.serving ? undefined : snapshot.message - state.progress = snapshot.serving ? undefined : snapshot.progress + // A render in flight is the only thing worth waiting for at this point, + // and until the first one lands nothing else is being reported at all. + const rendering = !!snapshot.pending && !snapshot.serving + state.awaitingFirstRender = rendering + state.note = snapshot.pending ? `rendering ${snapshot.pending.label}` : undefined + state.progress = rendering ? snapshot.progress : undefined state.phaseStartedAt = undefined state.phaseElapsedMs = undefined if (state.status === 'ready' || state.status === 'warming') { - state.status = snapshot.serving ? 'ready' : 'warming' + state.status = rendering ? 'warming' : 'ready' render() } return diff --git a/packages/nuxt-cli/src/dev/utils.ts b/packages/nuxt-cli/src/dev/utils.ts index 800e46fa3..43db211ea 100644 --- a/packages/nuxt-cli/src/dev/utils.ts +++ b/packages/nuxt-cli/src/dev/utils.ts @@ -546,8 +546,15 @@ export class NuxtDevServer extends EventEmitter { this.#inflightResponses.add(res) // A document that Nuxt itself answered is the first proof the app can be used. const document = isDocumentRequest(req) + // Rendering a page compiles the module graph on demand, which is silent and + // can take longer than the whole build did, so the request is reported for + // as long as it is in flight. + const pending = document ? this.#progress.startRequest(`${req.method || 'GET'} ${req.url || '/'}`) : undefined res.once('close', () => { this.#inflightResponses.delete(res) + if (pending !== undefined) { + this.#progress.finishRequest(pending) + } if (document && res.statusCode < 500) { this.#progress.setServing() } diff --git a/packages/nuxt-cli/src/utils/phase-reporter.ts b/packages/nuxt-cli/src/utils/phase-reporter.ts index 02b7bb4be..62e79c485 100644 --- a/packages/nuxt-cli/src/utils/phase-reporter.ts +++ b/packages/nuxt-cli/src/utils/phase-reporter.ts @@ -7,7 +7,9 @@ import { isCI } from 'std-env' import { formatDuration } from './formatting' import { logger } from './logger' +import { READY_MESSAGE } from './progress-snapshot' import { tapOutput } from './stdout' +import { terminalLink } from './terminal-link' const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] const FRAME_INTERVAL = 80 @@ -50,6 +52,8 @@ function formatElapsed(snapshot: ProgressSnapshot, drift: number): string { export interface PhaseReporter { update: (snapshot: ProgressSnapshot) => void + /** The URL to print alongside the summary, once one is known. */ + setURL: (url: string) => void stop: () => void } @@ -77,39 +81,65 @@ export function formatPhaseBreakdown(timings: PhaseTiming[]): string { .join(' · ') } +/** + * The URL the summary points at, so the address is on screen at the moment the + * user is being told they can click it rather than a screenful further up. + */ +function formatURL(url: string): string { + return ` ${styleText('dim', '\u2192')} ${styleText('cyan', terminalLink(url, url))}` +} + +function decapitalise(text: string): string { + return /^[A-Z][a-z]/.test(text) ? text[0]!.toLowerCase() + text.slice(1) : text +} + /** * The line a long-running command leaves behind once it is up. A command that * only builds never reaches `ready` and reports its own completion instead. */ -export function formatSummary(snapshot: ProgressSnapshot): string { +export function formatSummary(snapshot: ProgressSnapshot, url?: string): string { const breakdown = formatPhaseBreakdown(snapshot.timings) - const headline = `${snapshot.reload ? 'Reloaded' : 'Ready'} in ${formatDuration(snapshot.elapsed)}${ - snapshot.serving ? '' : styleText('dim', ' \u00B7 compiling the first request')}` + // Whatever the ready phase is still waiting on, if it is waiting on anything. + const detail = snapshot.serving || !snapshot.message || snapshot.message === READY_MESSAGE + ? '' + : styleText('dim', ` \u00B7 ${decapitalise(snapshot.message)}`) + const headline = `${snapshot.reload ? 'Reloaded' : 'Ready'} in ${formatDuration(snapshot.elapsed)}${detail}${ + url ? formatURL(url) : ''}` return breakdown ? `${headline}\n${styleText('dim', breakdown)}` : headline } /** * Report progress through a command's phases on a single line that updates in * place, collapsing to one summary line with a phase breakdown once the command - * reports itself ready. Falls back to sequential logs when the output is not an - * interactive terminal. + * reports itself ready, and then narrating the first page render, which is where + * the rest of the wait is spent. Falls back to sequential logs when the output + * is not an interactive terminal. */ export function createPhaseReporter(options: PhaseReporterOptions = {}): PhaseReporter { const stream = options.stream ?? process.stdout const animated = options.animated ?? isAnimationSupported(stream) let snapshot: ProgressSnapshot | undefined - let served = false + let summarised = false let lastPhase: string | undefined let lastMessage: string | undefined let lastLoggedAt = 0 let frame = 0 let dirty = false let painted: string | undefined + /** Whether the cursor is currently hidden for the transient line. */ + let hidden = false let timer: NodeJS.Timeout | undefined let stopped = false let receivedAt = Date.now() let pulse: NodeJS.Timeout | undefined + let url: string | undefined + /** The request the line is currently reporting, so it is announced once. */ + let narrating: string | undefined + /** Whether the wait was ever narrated, and so is worth closing off. */ + let narrated = false + /** When the render being waited on arrived, so the wait can be measured. */ + let renderStartedAt: number | undefined // Foreign output would otherwise be written on top of the transient line. The // tap sits below `consola.wrapAll()`, which `nuxt dev` installs, for two @@ -135,9 +165,16 @@ export function createPhaseReporter(options: PhaseReporterOptions = {}): PhaseRe } } - /** The line after the spinner glyph: the phase and how long it has taken. */ + /** + * The line after the spinner glyph: what the command is doing and how long it + * has been doing it. Once the summary is printed that is no longer a phase but + * the request being rendered, which has its own clock. + */ function describe(): string { - return `${snapshot!.message} ${styleText('dim', formatElapsed(snapshot!, Date.now() - receivedAt))}` + const pending = summarised ? snapshot!.pending : undefined + return pending + ? `rendering ${pending.label} ${styleText('dim', formatTicking(Date.now() - pending.startedAt))}` + : `${snapshot!.message} ${styleText('dim', formatElapsed(snapshot!, Date.now() - receivedAt))}` } function render() { @@ -158,12 +195,18 @@ export function createPhaseReporter(options: PhaseReporterOptions = {}): PhaseRe dirty = true } - if (animated) { + function animate() { + if (!animated || timer) { + return + } + hidden = true write(HIDE_CURSOR) timer = setInterval(render, FRAME_INTERVAL) timer.unref?.() } + animate() + /** * Repeat the phase in flight, with the time it has taken so far, so a long * silent stretch in a piped log still shows the build is alive. @@ -182,6 +225,7 @@ export function createPhaseReporter(options: PhaseReporterOptions = {}): PhaseRe pulse.unref?.() } + /** Take the line down, leaving the terminal as it was found. */ function restore() { clearInterval(pulse) pulse = undefined @@ -189,70 +233,107 @@ export function createPhaseReporter(options: PhaseReporterOptions = {}): PhaseRe clearInterval(timer) timer = undefined } - if (animated) { + if (animated && hidden) { + hidden = false clear() write(SHOW_CURSOR) - tap?.dispose() } } + function finish() { + stopped = true + restore() + tap?.dispose() + } + return { + setURL(next) { + url = next + }, update(next) { if (stopped) { return } - // The summary is printed; the only thing left to say is that the first - // request has been answered. - if (served && next.status !== 'ready') { + // The summary is printed; the only thing left to report is the first + // render, and a reload is narrated by whoever asked for it. + if (summarised && next.status !== 'ready') { return } snapshot = next receivedAt = Date.now() if (next.status === 'error') { - restore() - stopped = true + finish() return } - if (next.status === 'ready') { - if (served) { - if (next.serving) { - stopped = true - logger.success(`Serving in ${formatDuration(next.elapsed)}`) - } + if (next.status !== 'ready') { + if (animated) { + render() return } - restore() - served = true - stopped = next.serving - logger.success(formatSummary(next)) + + if (next.phase !== lastPhase) { + lastPhase = next.phase + log(next.message, next.message) + schedulePulse() + return + } + + // Within a phase the message is narration rather than progress, so it is + // only worth a line where the phase is long enough to have a heartbeat, + // and never more often than one. The phase clock goes with it: without an + // animated line it is the only sign the wait is still moving. + if (options.heartbeat && next.message !== lastMessage && Date.now() - lastLoggedAt >= options.heartbeat) { + log(`${next.message} ${styleText('dim', `(${formatElapsed(next, 0)})`)}`, next.message) + } return } - if (animated) { - render() + if (!summarised) { + restore() + summarised = true + narrated = !next.serving && next.message !== READY_MESSAGE + logger.success(formatSummary(next, url)) + } + + // Accepting requests is not answering one: until a page has been + // rendered, whatever the server is busy with is the wait the user is + // actually in, so it is reported rather than left silent. + if (next.serving) { + restore() + if (narrated) { + // Measured from the request rather than from startup: the server may + // have been sitting idle and ready for minutes before anyone asked. + logger.success(renderStartedAt === undefined + ? `Serving in ${formatDuration(next.elapsed)}` + : `First render in ${formatDuration(Date.now() - renderStartedAt)}`) + } + finish() return } - if (next.phase !== lastPhase) { - lastPhase = next.phase - log(next.message, next.message) - schedulePulse() + if (!next.pending) { + narrating = undefined + restore() return } - // Within a phase the message is narration rather than progress, so it is - // only worth a line where the phase is long enough to have a heartbeat, - // and never more often than one. The phase clock goes with it: without an - // animated line it is the only sign the wait is still moving. - if (options.heartbeat && next.message !== lastMessage && Date.now() - lastLoggedAt >= options.heartbeat) { - log(`${next.message} ${styleText('dim', `(${formatElapsed(next, 0)})`)}`, next.message) + narrated = true + renderStartedAt ??= next.pending.startedAt + if (animated) { + animate() + render() + return + } + // A pipe cannot redraw, so each request is announced once as it starts. + if (narrating !== next.pending.label) { + narrating = next.pending.label + logger.info(`Rendering ${next.pending.label}`) } }, stop() { - stopped = true - restore() + finish() }, } } diff --git a/packages/nuxt-cli/src/utils/progress-snapshot.ts b/packages/nuxt-cli/src/utils/progress-snapshot.ts index ed29ae02a..5b3ec9c4d 100644 --- a/packages/nuxt-cli/src/utils/progress-snapshot.ts +++ b/packages/nuxt-cli/src/utils/progress-snapshot.ts @@ -6,6 +6,19 @@ export type ProgressStatus = 'loading' | 'ready' | 'error' +/** + * The message of the phase a command ends in, so a reporter can tell a phase + * label from narration about what the phase is still waiting on. + */ +export const READY_MESSAGE: string = 'Ready' + +export interface PendingRender { + /** How the request reads to a user, e.g. `GET /about`. */ + label: string + /** When it arrived, so a consumer can tick the elapsed time itself. */ + startedAt: number +} + export interface PhaseTiming { phase: string message: string @@ -33,6 +46,13 @@ export interface ProgressSnapshot { * can be used yet. Always true for a command that only builds. */ serving: boolean + /** + * The request the server is busy with, once it has been busy long enough to + * be worth reporting. This is the only thing that happens between `ready` and + * the first page appearing, and on a cold start it is the longest wait of the + * whole load. Never set by a command that only builds. + */ + pending?: PendingRender timings: PhaseTiming[] error?: { name: string, message: string } } diff --git a/packages/nuxt-cli/test/unit/dev-progress.spec.ts b/packages/nuxt-cli/test/unit/dev-progress.spec.ts index 584baf737..fedb808a6 100644 --- a/packages/nuxt-cli/test/unit/dev-progress.spec.ts +++ b/packages/nuxt-cli/test/unit/dev-progress.spec.ts @@ -69,7 +69,6 @@ describe('devProgress', () => { progress.setReady() expect(progress.snapshot.status).toBe('ready') - expect(progress.snapshot.progress).toBe(1) expect(progress.timings.map(timing => timing.phase)).toEqual(['config', 'modules']) }) @@ -457,13 +456,62 @@ describe('devProgress', () => { expect(progress.snapshot.message).toBe('Ready') }) - it('should not wait for a render nobody is waiting for', () => { + it('should not say a render is being waited for when nothing is waiting', () => { const progress = new DevProgress() progress.start() progress.setReady() - expect(progress.snapshot.serving).toBe(true) - expect(progress.snapshot.progress).toBe(1) + expect(progress.snapshot.serving).toBe(false) + expect(progress.snapshot.progress).toBe(0.95) + expect(progress.snapshot.message).toBe('Ready') + }) + + it('should report a request that has been in flight long enough to notice', () => { + vi.useFakeTimers() + const progress = new DevProgress() + progress.start() + progress.setReady() + + const id = progress.startRequest('GET /') + expect(progress.snapshot.pending).toBeUndefined() + + vi.advanceTimersByTime(500) + expect(progress.snapshot.pending?.label).toBe('GET /') + + progress.finishRequest(id) + expect(progress.snapshot.pending).toBeUndefined() + vi.useRealTimers() + }) + + it('should say nothing about a request answered before it could be noticed', () => { + vi.useFakeTimers() + const progress = new DevProgress() + progress.start() + progress.setReady() + + const id = progress.startRequest('GET /') + vi.advanceTimersByTime(100) + progress.finishRequest(id) + vi.advanceTimersByTime(1000) + + expect(progress.snapshot.pending).toBeUndefined() + vi.useRealTimers() + }) + + it('should hand the line to whatever is still in flight', () => { + vi.useFakeTimers() + const progress = new DevProgress() + progress.start() + progress.setReady() + + const first = progress.startRequest('GET /') + progress.startRequest('GET /about') + vi.advanceTimersByTime(500) + expect(progress.snapshot.pending?.label).toBe('GET /') + + progress.finishRequest(first) + expect(progress.snapshot.pending?.label).toBe('GET /about') + vi.useRealTimers() }) it('should stop waiting once the page that was waiting has gone', () => { diff --git a/packages/nuxt-cli/test/unit/dev-tui.spec.ts b/packages/nuxt-cli/test/unit/dev-tui.spec.ts index 72e0562a9..f962aaaa3 100644 --- a/packages/nuxt-cli/test/unit/dev-tui.spec.ts +++ b/packages/nuxt-cli/test/unit/dev-tui.spec.ts @@ -74,6 +74,14 @@ describe('dev tui panel', () => { expect(lines.join('\n')).not.toContain('READY') }) + it('should say which request it is busy with', () => { + const warming = renderPanel({ ...READY, status: 'warming', awaitingFirstRender: true, note: 'rendering GET /' }, 80, 30).map(strip) + expect(warming.join('\n')).toContain('WARMUP rendering GET /') + + const ready = renderPanel({ ...READY, note: 'rendering GET /about' }, 80, 30).map(strip) + expect(ready.join('\n')).toContain('READY rendering GET /about') + }) + it('should not keep claiming how fast the last load was while rebuilding', () => { for (const status of ['building', 'restarting', 'error'] as const) { expect(strip(renderPanel({ ...READY, status }, 100, 30)[0]!)).not.toContain('ready in') diff --git a/packages/nuxt-cli/test/unit/phase-reporter.spec.ts b/packages/nuxt-cli/test/unit/phase-reporter.spec.ts index a784a1f01..5abbe7175 100644 --- a/packages/nuxt-cli/test/unit/phase-reporter.spec.ts +++ b/packages/nuxt-cli/test/unit/phase-reporter.spec.ts @@ -216,6 +216,82 @@ describe('phase reporter', () => { `) }) + it('should print the URL next to the summary', async () => { + const renderer = await render(() => { + const startup = reporter(true) + startup.setURL('http://localhost:3000/') + startup.update(snapshot()) + startup.update(snapshot({ status: 'ready', phase: 'ready', message: 'Ready', index: 6, elapsed: 2400 })) + }) + + expect(screen(renderer)).toMatchInlineSnapshot(` + "│ + ◆ Ready in 2.4s → http://localhost:3000/" + `) + }) + + it('should report a render the server is busy with after it is ready', async () => { + freezeClock() + const renderer = await render(() => { + const startup = reporter(true) + startup.update(snapshot()) + startup.update(snapshot({ status: 'ready', phase: 'ready', message: 'Ready', index: 6, elapsed: 2400 })) + startup.update(snapshot({ + status: 'ready', + phase: 'ready', + message: 'Ready', + index: 6, + elapsed: 2400, + pending: { label: 'GET /', startedAt: Date.now() - 4200 }, + })) + }) + + expect(screen(renderer)).toMatch(/^. rendering GET \/ 4\.2s$/m) + }) + + it('should announce a render once where the line cannot be redrawn', async () => { + const renderer = await render(() => { + const startup = reporter(false) + startup.update(snapshot({ status: 'ready', phase: 'ready', message: 'Ready', index: 6, elapsed: 2400 })) + const pending = { label: 'GET /', startedAt: Date.now() - 6400 } + startup.update(snapshot({ status: 'ready', phase: 'ready', message: 'Ready', index: 6, elapsed: 2400, pending })) + startup.update(snapshot({ status: 'ready', phase: 'ready', message: 'Ready', index: 6, elapsed: 2400, pending })) + startup.update(snapshot({ status: 'ready', phase: 'ready', message: 'Ready', index: 6, serving: true, elapsed: 16_800 })) + }) + + expect(screen(renderer)).toMatchInlineSnapshot(` + "│ + ◆ Ready in 2.4s + │ + ● Rendering GET / + │ + ◆ First render in 6.4s" + `) + }) + + it('should announce being ready once, however many times it is told', async () => { + const renderer = await render(() => { + const startup = reporter(false) + const ready = snapshot({ status: 'ready', phase: 'ready', message: 'Ready', index: 6, elapsed: 2400 }) + startup.update(ready) + startup.update(ready) + startup.update(snapshot({ status: 'loading', phase: 'config', message: 'Loading Nuxt config' })) + startup.update({ ...ready, reload: true }) + }) + + expect(screen(renderer).match(/Ready in/g)).toHaveLength(1) + }) + + it('should not close off a wait it never reported', async () => { + const renderer = await render(() => { + const startup = reporter(false) + startup.update(snapshot({ status: 'ready', phase: 'ready', message: 'Ready', index: 6, elapsed: 2400 })) + startup.update(snapshot({ status: 'ready', phase: 'ready', message: 'Ready', index: 6, serving: true, elapsed: 2600 })) + }) + + expect(screen(renderer)).toBe('│\n◆ Ready in 2.4s') + }) + it('should say nothing more after a build error, which is reported separately', async () => { const renderer = await render(() => { const startup = reporter(true) From 78ec84e06b11c47317ac8653720c5671f0f3b6dd Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 24 Aug 2026 10:34:36 +0000 Subject: [PATCH 2/7] feat(dev): forward the render in flight from the fork that is serving --- packages/nuxt-cli/src/commands/dev.ts | 3 +++ packages/nuxt-cli/src/dev/index.ts | 14 ++++++++++++++ packages/nuxt-cli/src/dev/tui/controller.ts | 4 ++++ packages/nuxt-cli/src/dev/tui/index.ts | 8 ++++++++ packages/nuxt-cli/src/dev/utils.ts | 2 ++ packages/nuxt-cli/test/unit/dev-tui.spec.ts | 20 ++++++++++++++++++++ 6 files changed, 51 insertions(+) diff --git a/packages/nuxt-cli/src/commands/dev.ts b/packages/nuxt-cli/src/commands/dev.ts index f67254e99..988a51c86 100644 --- a/packages/nuxt-cli/src/commands/dev.ts +++ b/packages/nuxt-cli/src/commands/dev.ts @@ -351,6 +351,9 @@ const command = defineCommand({ else if (message.type === 'nuxt:internal:dev:building') { devUI.setStatus(message.building ? 'building' : 'ready') } + else if (message.type === 'nuxt:internal:dev:rendering') { + devUI.setRendering(message.pending) + } else if (message.type === 'nuxt:internal:dev:ready' || message.type === 'nuxt:internal:dev:loading:error') { serving = true if (message.type === 'nuxt:internal:dev:ready' && startTime) { diff --git a/packages/nuxt-cli/src/dev/index.ts b/packages/nuxt-cli/src/dev/index.ts index c85839d81..bfbcf1915 100644 --- a/packages/nuxt-cli/src/dev/index.ts +++ b/packages/nuxt-cli/src/dev/index.ts @@ -328,6 +328,20 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti routes.emit(payload) }) + // The panel is painted by the parent, and a render in flight is all this + // fork has to report between being ready and having answered. + if (ipc.enabled) { + let reported: string | undefined + devServer.progress.onUpdate(({ status, pending }) => { + const rendering = status === 'ready' ? pending : undefined + if (rendering?.label === reported) { + return + } + reported = rendering?.label + ipc.send({ type: 'nuxt:internal:dev:rendering', pending: rendering }) + }) + } + // A dev server serves a request per module on a cold page load, so requests // are batched rather than sent one IPC message at a time. let batch: DevRequestEvent[] = [] diff --git a/packages/nuxt-cli/src/dev/tui/controller.ts b/packages/nuxt-cli/src/dev/tui/controller.ts index 3edbf388a..603e214ee 100644 --- a/packages/nuxt-cli/src/dev/tui/controller.ts +++ b/packages/nuxt-cli/src/dev/tui/controller.ts @@ -1,3 +1,4 @@ +import type { PendingRender } from '../../utils/progress-snapshot' import type { ServerLogEvent } from '../log-channel' import type { ShortcutContext } from '../shortcuts' import type { DevRequestEvent, DevRoutes } from '../utils' @@ -18,6 +19,8 @@ export interface DevUIController { pushRequests: (requests: DevRequestEvent[]) => void /** Replace the routes shown in the route view. */ setRoutes: (routes: DevRoutes) => void + /** Report the render the server is busy with, or that it is busy with none. */ + setRendering: (pending?: PendingRender) => void } /** What the plain fallback answers to everything the UI would have shown. */ @@ -27,6 +30,7 @@ export const NOOP_CONTROLLER: DevUIController = { pushServerLog: () => {}, pushRequests: () => {}, setRoutes: () => {}, + setRendering: () => {}, } /** diff --git a/packages/nuxt-cli/src/dev/tui/index.ts b/packages/nuxt-cli/src/dev/tui/index.ts index 9899cbc00..7e20ee51b 100644 --- a/packages/nuxt-cli/src/dev/tui/index.ts +++ b/packages/nuxt-cli/src/dev/tui/index.ts @@ -582,6 +582,14 @@ export function setupDevUI(context: ShortcutContext, options: DevUIOptions = {}) activityTimer.unref?.() }, setRoutes: payload => routeOverlay.setRoutes(payload), + setRendering: (pending) => { + // A build in flight is already the more interesting thing to report, and + // the badge it shows must not be replaced by a request that outlives it. + if (state.status !== 'ready') { + return + } + update({ note: pending ? `rendering ${pending.label}` : undefined }) + }, } } diff --git a/packages/nuxt-cli/src/dev/utils.ts b/packages/nuxt-cli/src/dev/utils.ts index 43db211ea..b30e80fb0 100644 --- a/packages/nuxt-cli/src/dev/utils.ts +++ b/packages/nuxt-cli/src/dev/utils.ts @@ -4,6 +4,7 @@ import type { NitroDevServer } from 'nitropack' import type { FSWatcher, Stats } from 'node:fs' import type { Server as HttpServer, IncomingMessage, RequestListener, ServerResponse } from 'node:http' +import type { PendingRender } from '../utils/progress-snapshot' import type { ResolvedCertificate } from './cert' import type { InspectOptions } from './inspect' import type { BoundServer, DevListenOverrides, Listener, ListenOptions, ListenURL } from './listen' @@ -95,6 +96,7 @@ export type NuxtDevIPCMessage | { type: 'nuxt:internal:dev:requests', requests: DevRequestEvent[] } | { type: 'nuxt:internal:dev:routes', payload: DevRoutes } | { type: 'nuxt:internal:dev:building', building: boolean } + | { type: 'nuxt:internal:dev:rendering', pending?: PendingRender } export interface NuxtDevContext { cwd: string diff --git a/packages/nuxt-cli/test/unit/dev-tui.spec.ts b/packages/nuxt-cli/test/unit/dev-tui.spec.ts index f962aaaa3..789d790c1 100644 --- a/packages/nuxt-cli/test/unit/dev-tui.spec.ts +++ b/packages/nuxt-cli/test/unit/dev-tui.spec.ts @@ -2295,6 +2295,26 @@ describe('request failures on the panel', () => { }) }) + it('should report a render forwarded from the fork that is serving', async () => { + await withPanel(async (ui, settle) => { + ui.setStatus('ready') + ui.setRendering({ label: 'GET /about', startedAt: Date.now() }) + expect(await settle()).toContain('rendering GET /about') + + ui.setRendering(undefined) + expect(await settle()).toContain('watching for changes') + }) + }) + + it('should not let a render replace what a build is reporting', async () => { + await withPanel(async (ui, settle) => { + ui.setStatus('building', 'compiling changes') + ui.setRendering({ label: 'GET /about', startedAt: Date.now() }) + + expect(await settle()).not.toContain('rendering GET /about') + }) + }) + it('should report a failed app request', async () => { await withPanel(async (ui, settle) => { ui.setStatus('ready') From cc5ef69ea2b97a072a19663310fe8810b67f77be Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 25 Aug 2026 18:09:38 +0000 Subject: [PATCH 3/7] fix(dev): hold the render in flight on the panel until it lands --- packages/nuxt-cli/src/dev/tui/index.ts | 37 ++++++++++------ packages/nuxt-cli/src/dev/tui/panel.ts | 43 ++++++++++++++++-- packages/nuxt-cli/src/dev/tui/session.ts | 40 +++++++++++++---- packages/nuxt-cli/test/unit/dev-tui.spec.ts | 49 ++++++++++++++++++--- 4 files changed, 140 insertions(+), 29 deletions(-) diff --git a/packages/nuxt-cli/src/dev/tui/index.ts b/packages/nuxt-cli/src/dev/tui/index.ts index 7e20ee51b..1ec1eb6ab 100644 --- a/packages/nuxt-cli/src/dev/tui/index.ts +++ b/packages/nuxt-cli/src/dev/tui/index.ts @@ -146,6 +146,10 @@ export function setupDevUI(context: ShortcutContext, options: DevUIOptions = {}) update({}) } + // Progress writes the panel state itself; going through `update` is what arms + // the animation for whatever it has just put there. + session.onProgressChange(refresh) + function clearActivity(): void { update({ active: false }) } @@ -156,16 +160,22 @@ export function setupDevUI(context: ShortcutContext, options: DevUIOptions = {}) frame: (state.frame ?? 0) + 1, elapsedMs: working ? Date.now() - (state.loadStartedAt ?? sessionStart) : state.elapsedMs, phaseElapsedMs: working && state.phaseStartedAt !== undefined ? Date.now() - state.phaseStartedAt : state.phaseElapsedMs, + renderingMs: state.rendering && Date.now() - state.rendering.startedAt, }) } - /** Animate the mark only while the server or a task is working, on screen. */ + /** + * Animate the mark only while something is in flight, on screen. A request + * being rendered counts: the server is not loading, but it is the only thing + * happening, and a still panel in front of a slow page reads as a hung one. + */ function syncAnimation(): void { - const working = (state.status !== 'ready' && state.status !== 'error' && !openOverlay()) || (!!state.task && !openOverlay()) - // Waiting on the first render is measured in seconds, sometimes tens of - // them, which is too long to spend a build's frame rate on: the panel only - // has to look alive. - const interval = state.status === 'warming' ? LOGO_FRAME_MS * WARMUP_FRAME_RATIO : LOGO_FRAME_MS + const busy = (state.status !== 'ready' && state.status !== 'error') || !!state.task || !!state.rendering + const working = busy && !openOverlay() + // Waiting on a render is measured in seconds, sometimes tens of them, which + // is too long to spend a build's frame rate on: the panel only has to look + // alive. + const interval = state.status === 'warming' || state.rendering ? LOGO_FRAME_MS * WARMUP_FRAME_RATIO : LOGO_FRAME_MS if (working && animation && interval !== animationInterval) { clearInterval(animation) animation = undefined @@ -283,7 +293,10 @@ export function setupDevUI(context: ShortcutContext, options: DevUIOptions = {}) }) context.onReady(() => { - const warming = state.awaitingFirstRender === true + // Whether anything is still being waited for is progress's to say: a ready + // listener only knows the socket is up, and a server nobody has asked for a + // page yet is not warming up, it is idle. + const warming = state.status === 'warming' update({ status: warming ? 'warming' : 'ready', note: warming ? state.note : undefined, @@ -583,12 +596,10 @@ export function setupDevUI(context: ShortcutContext, options: DevUIOptions = {}) }, setRoutes: payload => routeOverlay.setRoutes(payload), setRendering: (pending) => { - // A build in flight is already the more interesting thing to report, and - // the badge it shows must not be replaced by a request that outlives it. - if (state.status !== 'ready') { - return - } - update({ note: pending ? `rendering ${pending.label}` : undefined }) + update({ + rendering: pending && { label: pending.label, startedAt: pending.startedAt }, + renderingMs: pending && Date.now() - pending.startedAt, + }) }, } } diff --git a/packages/nuxt-cli/src/dev/tui/panel.ts b/packages/nuxt-cli/src/dev/tui/panel.ts index f1ba15d84..ac66ffac3 100644 --- a/packages/nuxt-cli/src/dev/tui/panel.ts +++ b/packages/nuxt-cli/src/dev/tui/panel.ts @@ -94,6 +94,15 @@ export interface PanelState { active?: boolean /** Replaces the badge's standing description, for a restart reason. */ note?: string + /** + * The request being rendered right now, and when it arrived. Held apart from + * `note` and `status`, which belong to the load: a request in flight is not a + * state of the load, and a load reporting itself ready again mid-render must + * not take it off the panel. + */ + rendering?: { label: string, startedAt: number } + /** How long that render has been going, for its ticking clock. */ + renderingMs?: number /** * Feedback in place of the badge's standing description. Passing unless it * carries a `label`, which marks something waiting on the user: the label @@ -228,6 +237,13 @@ const TASK_FRAMES_ASCII = ['|', '/', '-', '\\'] as const /** How long a phase runs before its own elapsed time is worth a mention. */ const PHASE_ELAPSED_THRESHOLD = 2500 +/** + * How long a render runs before its own clock is worth showing. Lower than a + * phase's, because a render is only reported once it has already been in flight + * long enough to notice, and the clock is the only thing that moves. + */ +const RENDER_ELAPSED_THRESHOLD = 1000 + function renderProgress(state: PanelState, columns: number): string { const fraction = Math.min(1, Math.max(0, state.progress ?? 0)) const filled = Math.round(fraction * PROGRESS_BAR_WIDTH) @@ -315,10 +331,31 @@ function renderStatus(state: PanelState, columns: number): string { ) } - const badge = BADGES[state.status] - const description = state.notice ? renderNotice(state) : styleText(MUTED, decapitalise(state.note || badge.note) + renderPhaseElapsed(state)) + // A render is only worth reporting over a server with nothing else to say; + // a load in flight is the more important thing and keeps the line. + const rendering = state.status === 'ready' || state.status === 'warming' ? state.rendering : undefined + const badge = rendering && state.awaitingFirstRender ? BADGES.warming : BADGES[state.status] + const description = state.notice + ? renderNotice(state) + : rendering + ? styleText(MUTED, `rendering ${rendering.label}${renderRenderElapsed(state)}`) + : styleText(MUTED, decapitalise(state.note || badge.note) + renderPhaseElapsed(state)) const head = ` ${styleText(badge.style, ` ${badge.label} `)} ${description}` - return truncate(head + renderTicker(state, columns - visibleWidth(head)), columns) + // The request in flight is the more interesting one, and it is usually the + // same URL as the last: printing both reads as a stutter. + return truncate(head + (rendering ? '' : renderTicker(state, columns - visibleWidth(head))), columns) +} + +/** + * How long the render in flight has taken. The only thing that moves on a panel + * whose server is up and waiting on a page, so it is what says the wait is + * progressing rather than stuck. + */ +function renderRenderElapsed(state: PanelState): string { + if (state.renderingMs === undefined || state.renderingMs < RENDER_ELAPSED_THRESHOLD) { + return '' + } + return ` \u00B7 ${(state.renderingMs / 1000).toFixed(1)}s` } /** diff --git a/packages/nuxt-cli/src/dev/tui/session.ts b/packages/nuxt-cli/src/dev/tui/session.ts index ac3b0e1b7..737e421c0 100644 --- a/packages/nuxt-cli/src/dev/tui/session.ts +++ b/packages/nuxt-cli/src/dev/tui/session.ts @@ -12,6 +12,7 @@ import { consola } from 'consola' import { KEEPS_PROCESS_ALIVE } from '../../utils/errors' import { debug, isEmittingCliLog, setLoggerImpl } from '../../utils/logger' import { getPkgVersion } from '../../utils/pkg' +import { READY_MESSAGE } from '../../utils/progress-snapshot' import { startupElapsedMs } from '../../utils/startup-clock' import { resolveBackground } from '../../utils/terminal-theme' import { currentRequest, isServingRequest } from '../serving-state' @@ -86,6 +87,12 @@ export interface DevUISession { stopStartupTicker: () => void /** Narrate the current startup phase while the server is loading. */ reportProgress: (snapshot: ProgressSnapshot) => void + /** + * Repaint through the controller instead of {@link render} whenever progress + * changes what is on the panel, so the controller can re-arm the animation it + * owns: a render in flight is work, and a still panel reads as a hung one. + */ + onProgressChange: (listener: () => void) => void /** * Show the bound address the moment the socket answers, spinning until the * resolved config confirms it. The full URL block replaces it on ready. @@ -176,22 +183,36 @@ export function beginDevUI(options: DevUISupportOptions & { version?: string, cw surface.render(renderPanel(state, process.stdout.columns || 80, process.stdout.rows || 24)) } + let progressListener: (() => void) | undefined + + /** Repaint through the controller where one is attached, so it sees the change. */ + function repaint(): void { + if (progressListener) { + progressListener() + return + } + render() + } + function reportProgress(snapshot: ProgressSnapshot): void { if (snapshot.status === 'ready') { // Between the server accepting requests and answering one there is // nothing to watch but a badge, so it says which of the two has happened. - // A render in flight is the only thing worth waiting for at this point, - // and until the first one lands nothing else is being reported at all. - const rendering = !!snapshot.pending && !snapshot.serving - state.awaitingFirstRender = rendering - state.note = snapshot.pending ? `rendering ${snapshot.pending.label}` : undefined - state.progress = rendering ? snapshot.progress : undefined + // Something already waiting for a page is a state of the load; the request + // being rendered is not, and is held separately so that a load reporting + // itself ready again cannot take it off the panel. + const waiting = !snapshot.serving && snapshot.message !== READY_MESSAGE + state.awaitingFirstRender = !snapshot.serving + state.note = waiting ? snapshot.message : undefined + state.progress = waiting ? snapshot.progress : undefined + state.rendering = snapshot.pending && { label: snapshot.pending.label, startedAt: snapshot.pending.startedAt } + state.renderingMs = snapshot.pending && Date.now() - snapshot.pending.startedAt state.phaseStartedAt = undefined state.phaseElapsedMs = undefined if (state.status === 'ready' || state.status === 'warming') { - state.status = rendering ? 'warming' : 'ready' - render() + state.status = waiting ? 'warming' : 'ready' } + repaint() return } if (snapshot.status !== 'loading') { @@ -413,6 +434,9 @@ export function beginDevUI(options: DevUISupportOptions & { version?: string, cw expectRender, stopStartupTicker, reportProgress, + onProgressChange: (listener) => { + progressListener = listener + }, reportListening, teardown, onTeardown: task => void teardownTasks.push(task), diff --git a/packages/nuxt-cli/test/unit/dev-tui.spec.ts b/packages/nuxt-cli/test/unit/dev-tui.spec.ts index 789d790c1..53d64a4c5 100644 --- a/packages/nuxt-cli/test/unit/dev-tui.spec.ts +++ b/packages/nuxt-cli/test/unit/dev-tui.spec.ts @@ -74,12 +74,38 @@ describe('dev tui panel', () => { expect(lines.join('\n')).not.toContain('READY') }) - it('should say which request it is busy with', () => { - const warming = renderPanel({ ...READY, status: 'warming', awaitingFirstRender: true, note: 'rendering GET /' }, 80, 30).map(strip) - expect(warming.join('\n')).toContain('WARMUP rendering GET /') + it('should say which request it is busy with, and for how long', () => { + const first = renderPanel({ ...READY, awaitingFirstRender: true, rendering: { label: 'GET /', startedAt: 0 }, renderingMs: 6400 }, 80, 30).map(strip) + expect(first.join('\n')).toContain('WARMUP rendering GET / · 6.4s') - const ready = renderPanel({ ...READY, note: 'rendering GET /about' }, 80, 30).map(strip) - expect(ready.join('\n')).toContain('READY rendering GET /about') + const later = renderPanel({ ...READY, rendering: { label: 'GET /about', startedAt: 0 }, renderingMs: 1200 }, 80, 30).map(strip) + expect(later.join('\n')).toContain('READY rendering GET /about · 1.2s') + }) + + it('should keep the last request off the line while one is in flight', () => { + const lines = renderPanel({ + ...READY, + rendering: { label: 'GET /', startedAt: 0 }, + renderingMs: 6700, + lastRequest: { method: 'GET', url: '/', status: 200, duration: 8442 }, + }, 100, 30).map(strip) + + expect(lines.join('\n')).toContain('READY rendering GET / · 6.7s') + expect(lines.join('\n')).not.toContain('8442ms') + }) + + it('should not put a clock on a render that has only just arrived', () => { + const lines = renderPanel({ ...READY, rendering: { label: 'GET /', startedAt: 0 }, renderingMs: 40 }, 80, 30).map(strip) + + expect(lines.join('\n')).toContain('READY rendering GET /') + expect(lines.join('\n')).not.toContain('0.0s') + }) + + it('should let a load in flight keep the status line from a render', () => { + const lines = renderPanel({ ...READY, status: 'building', note: 'nuxt.config.ts changed', rendering: { label: 'GET /', startedAt: 0 }, renderingMs: 6400 }, 80, 30).map(strip) + + expect(lines.join('\n')).toContain('BUILDING nuxt.config.ts changed') + expect(lines.join('\n')).not.toContain('rendering GET /') }) it('should not keep claiming how fast the last load was while rebuilding', () => { @@ -2315,6 +2341,19 @@ describe('request failures on the panel', () => { }) }) + it('should keep reporting a render when the server reports itself ready again', async () => { + await withPanel(async (ui, settle) => { + ui.setStatus('ready') + ui.setRendering({ label: 'GET /', startedAt: Date.now() }) + // The bundler reloads while it serves the first document, which is a + // `building` event either side of the render it is serving. + ui.setStatus('building') + ui.setStatus('ready') + + expect(await settle()).toContain('rendering GET /') + }) + }) + it('should report a failed app request', async () => { await withPanel(async (ui, settle) => { ui.setStatus('ready') From 96907f4fedbf80dcd7b526e97a93fe11e1570c5f Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 25 Aug 2026 18:09:38 +0000 Subject: [PATCH 4/7] feat(dev): repeat the render in flight where the line cannot be redrawn --- capture/output/nuxt-dev-plain-restart.svg | 44 +++++++++---------- capture/output/nuxt-dev-plain-restart.txt | 4 +- capture/output/nuxt-dev-plain-static.svg | 2 +- capture/output/nuxt-dev-plain-static.txt | 4 +- capture/output/nuxt-dev-plain.svg | 37 ++++++++-------- capture/output/nuxt-dev-plain.txt | 4 +- packages/nuxt-cli/src/utils/phase-reporter.ts | 26 ++++++++--- .../nuxt-cli/test/unit/phase-reporter.spec.ts | 13 ++++++ 8 files changed, 80 insertions(+), 54 deletions(-) diff --git a/capture/output/nuxt-dev-plain-restart.svg b/capture/output/nuxt-dev-plain-restart.svg index 37ba776a1..7b1e3cd7e 100644 --- a/capture/output/nuxt-dev-plain-restart.svg +++ b/capture/output/nuxt-dev-plain-restart.svg @@ -16,27 +16,27 @@ svg{--bg:#ffffff;--fg:#24292f;--chrome:#f6f8fa;--dot:#d0d7de} nuxt dev (plain output, restart on config change) - - Starting Nuxt... 0.0s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose Setting up modules 0.0s · 0.2s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Preparing app 0.0s · 0.3s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Preparing app 0.2s · 0.5s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Generating types 0.0s · 0.7s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Generating types 0.1s · 0.8s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Bundling app 0.0s · 0.9s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 37ms Vite server built in 12ms Building server 0.0s · 1.1s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 37ms Vite server built in 12ms Building server 0.1s · 1.1s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 37ms Vite server built in 12ms Building server 0.2s · 1.3s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 37ms Vite server built in 12ms Building server 0.3s · 1.4s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 37ms Vite server built in 12ms Building server 0.4s · 1.4s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 37ms Vite server built in 12ms Building server 0.5s · 1.5s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 37ms Vite server built in 12ms Building server 0.6s · 1.7s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 37ms Vite server built in 12ms Nuxt Nitro server built in 862ms nitro Ready in 2.02sconfig 155ms · modules 158ms · app 334ms · types 257ms · bundle 161ms · server 954mspressh + enterto see available shortcuts Vite server warmed up in 15ms Vite client warmed up in 31ms -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 37ms Vite server built in 12ms Nuxt Nitro server built in 862ms nitro Ready in 2.02sconfig 155ms · modules 158ms · app 334ms · types 257ms · bundle 161ms · server 954mspressh + enterto see available shortcuts Vite server warmed up in 15ms Vite client warmed up in 31ms nuxt.config.ts changed. Reloading Nuxt... -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 37ms Vite server built in 12ms Nuxt Nitro server built in 862ms nitro Ready in 2.02sconfig 155ms · modules 158ms · app 334ms · types 257ms · bundle 161ms · server 954mspressh + enterto see available shortcuts Vite server warmed up in 15ms Vite client warmed up in 31ms nuxt.config.ts changed. Reloading Nuxt... Re-optimizing dependencies because vite config has changed Vite client built in 15ms Vite server built in 16ms -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 37ms Vite server built in 12ms Nuxt Nitro server built in 862ms nitro Ready in 2.02sconfig 155ms · modules 158ms · app 334ms · types 257ms · bundle 161ms · server 954mspressh + enterto see available shortcuts Vite server warmed up in 15ms Vite client warmed up in 31ms nuxt.config.ts changed. Reloading Nuxt... Re-optimizing dependencies because vite config has changed Vite client built in 15ms Vite server built in 16ms Nuxt Nitro server built in 636ms nitro Vite server warmed up in 2ms Vite client warmed up in 7ms -Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 37ms Vite server built in 12ms Nuxt Nitro server built in 862ms nitro Ready in 2.02sconfig 155ms · modules 158ms · app 334ms · types 257ms · bundle 161ms · server 954mspressh + enterto see available shortcuts Vite server warmed up in 15ms Vite client warmed up in 31ms nuxt.config.ts changed. Reloading Nuxt... Re-optimizing dependencies because vite config has changed Vite client built in 15ms Vite server built in 16ms Nuxt Nitro server built in 636ms nitro Vite server warmed up in 2ms Vite client warmed up in 7ms nuxt.config.ts changed. Reloading Nuxt... ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) - Vite client built in 37ms Vite server built in 12ms Nuxt Nitro server built in 862ms nitro Ready in 2.02sconfig 155ms · modules 158ms · app 334ms · types 257ms · bundle 161ms · server 954mspressh + enterto see available shortcuts Vite server warmed up in 15ms Vite client warmed up in 31ms nuxt.config.ts changed. Reloading Nuxt... Re-optimizing dependencies because vite config has changed Vite client built in 15ms Vite server built in 16ms Nuxt Nitro server built in 636ms nitro Vite server warmed up in 2ms Vite client warmed up in 7ms nuxt.config.ts changed. Reloading Nuxt... ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed (x2) Vite client built in 14ms Vite server built in 10ms - Nuxt Nitro server built in 862ms nitro Ready in 2.02sconfig 155ms · modules 158ms · app 334ms · types 257ms · bundle 161ms · server 954mspressh + enterto see available shortcuts Vite server warmed up in 15ms Vite client warmed up in 31ms nuxt.config.ts changed. Reloading Nuxt... Re-optimizing dependencies because vite config has changed Vite client built in 15ms Vite server built in 16ms Nuxt Nitro server built in 636ms nitro Vite server warmed up in 2ms Vite client warmed up in 7ms nuxt.config.ts changed. Reloading Nuxt... ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed (x2) Vite client built in 14ms Vite server built in 10ms Nuxt Nitro server built in 604ms nitro Vite server warmed up in 2ms Vite client warmed up in 3ms + + Starting Nuxt... 0.0s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose Setting up modules 0.0s · 0.1s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Preparing app 0.0s · 0.3s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Preparing app 0.2s · 0.5s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Generating types 0.0s · 0.6s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Bundling app 0.0s · 0.8s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 34ms Vite server built in 13ms Building server 0.0s · 0.9s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 34ms Vite server built in 13ms Building server 0.0s · 1.0s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 34ms Vite server built in 13ms Building server 0.1s · 1.0s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 34ms Vite server built in 13ms Building server 0.2s · 1.1s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 34ms Vite server built in 13ms Building server 0.3s · 1.2s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 34ms Vite server built in 13ms Building server 0.4s · 1.3s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 34ms Vite server built in 13ms Building server 0.5s · 1.4s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 34ms Vite server built in 13ms Building server 0.6s · 1.5s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 34ms Vite server built in 13ms Nuxt Nitro server built in 763ms nitro Ready in 1.76s http://localhost:3000/config 133ms · modules 137ms · app 288ms · types 210ms · bundle 152ms · server 844mspressh + enterto see available shortcuts Vite server warmed up in 13ms Vite client warmed up in 19ms +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 34ms Vite server built in 13ms Nuxt Nitro server built in 763ms nitro Ready in 1.76s http://localhost:3000/config 133ms · modules 137ms · app 288ms · types 210ms · bundle 152ms · server 844mspressh + enterto see available shortcuts Vite server warmed up in 13ms Vite client warmed up in 19ms nuxt.config.ts changed. Reloading Nuxt... +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 34ms Vite server built in 13ms Nuxt Nitro server built in 763ms nitro Ready in 1.76s http://localhost:3000/config 133ms · modules 137ms · app 288ms · types 210ms · bundle 152ms · server 844mspressh + enterto see available shortcuts Vite server warmed up in 13ms Vite client warmed up in 19ms nuxt.config.ts changed. Reloading Nuxt... Re-optimizing dependencies because vite config has changed Vite client built in 22ms Vite server built in 16ms +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 34ms Vite server built in 13ms Nuxt Nitro server built in 763ms nitro Ready in 1.76s http://localhost:3000/config 133ms · modules 137ms · app 288ms · types 210ms · bundle 152ms · server 844mspressh + enterto see available shortcuts Vite server warmed up in 13ms Vite client warmed up in 19ms nuxt.config.ts changed. Reloading Nuxt... Re-optimizing dependencies because vite config has changed Vite client built in 22ms Vite server built in 16ms Nuxt Nitro server built in 599ms nitro Vite server warmed up in 2ms Vite client warmed up in 6ms +Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 34ms Vite server built in 13ms Nuxt Nitro server built in 763ms nitro Ready in 1.76s http://localhost:3000/config 133ms · modules 137ms · app 288ms · types 210ms · bundle 152ms · server 844mspressh + enterto see available shortcuts Vite server warmed up in 13ms Vite client warmed up in 19ms nuxt.config.ts changed. Reloading Nuxt... Re-optimizing dependencies because vite config has changed Vite client built in 22ms Vite server built in 16ms Nuxt Nitro server built in 599ms nitro Vite server warmed up in 2ms Vite client warmed up in 6ms nuxt.config.ts changed. Reloading Nuxt... ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) + Vite client built in 34ms Vite server built in 13ms Nuxt Nitro server built in 763ms nitro Ready in 1.76s http://localhost:3000/config 133ms · modules 137ms · app 288ms · types 210ms · bundle 152ms · server 844mspressh + enterto see available shortcuts Vite server warmed up in 13ms Vite client warmed up in 19ms nuxt.config.ts changed. Reloading Nuxt... Re-optimizing dependencies because vite config has changed Vite client built in 22ms Vite server built in 16ms Nuxt Nitro server built in 599ms nitro Vite server warmed up in 2ms Vite client warmed up in 6ms nuxt.config.ts changed. Reloading Nuxt... ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed (x2) Vite client built in 13ms Vite server built in 11ms + Nuxt Nitro server built in 763ms nitro Ready in 1.76s http://localhost:3000/config 133ms · modules 137ms · app 288ms · types 210ms · bundle 152ms · server 844mspressh + enterto see available shortcuts Vite server warmed up in 13ms Vite client warmed up in 19ms nuxt.config.ts changed. Reloading Nuxt... Re-optimizing dependencies because vite config has changed Vite client built in 22ms Vite server built in 16ms Nuxt Nitro server built in 599ms nitro Vite server warmed up in 2ms Vite client warmed up in 6ms nuxt.config.ts changed. Reloading Nuxt... ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed (x2) Vite client built in 13ms Vite server built in 11ms Nuxt Nitro server built in 555ms nitro Vite server warmed up in 3ms Vite client warmed up in 4ms diff --git a/capture/output/nuxt-dev-plain-restart.txt b/capture/output/nuxt-dev-plain-restart.txt index 8910a3918..ed8466e38 100644 --- a/capture/output/nuxt-dev-plain-restart.txt +++ b/capture/output/nuxt-dev-plain-restart.txt @@ -1,4 +1,4 @@ -styles: 87e6a8422be5aa0c +styles: 243359a4e417d924 press h + enter to see available shortcuts ➜ DevTools: press Shift + Alt + D in the browser (x.y.z) ➜ Local: http://localhost:3000/ @@ -10,7 +10,7 @@ styles: 87e6a8422be5aa0c ℹ nuxt.config.ts changed. Reloading Nuxt... │ │ config 42 ms · modules 42 ms · app 42 ms · types 42 ms · bundle 42 ms · server 42 ms -◆ Ready in 42 ms +◆ Ready in 42 ms → http://localhost:3000/ ● Nuxt x.y.z (with Nitro x.y.z, Vite x.y.z and Vue x.y.z) ✔ Nuxt Nitro server built in 42 ms nitro ✔ Vite client built in 42 ms diff --git a/capture/output/nuxt-dev-plain-static.svg b/capture/output/nuxt-dev-plain-static.svg index b15089f6e..351aa0a3c 100644 --- a/capture/output/nuxt-dev-plain-static.svg +++ b/capture/output/nuxt-dev-plain-static.svg @@ -16,6 +16,6 @@ svg{--bg:#ffffff;--fg:#24292f;--chrome:#f6f8fa;--dot:#d0d7de} nuxt dev (plain output, ready) -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 37ms Vite server built in 13ms Nuxt Nitro server built in 866ms nitro Ready in 2.03sconfig 166ms · modules 164ms · app 326ms · types 254ms · bundle 159ms · server 962mspressh + enterto see available shortcuts Vite server warmed up in 19ms Vite client warmed up in 29ms +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Vite client built in 34ms Vite server built in 12ms Nuxt Nitro server built in 790ms nitro Ready in 1.79s http://localhost:3000/config 130ms · modules 136ms · app 277ms · types 218ms · bundle 149ms · server 878mspressh + enterto see available shortcuts Vite server warmed up in 22ms Vite client warmed up in 42ms diff --git a/capture/output/nuxt-dev-plain-static.txt b/capture/output/nuxt-dev-plain-static.txt index 3fcfde96d..f1bf7d0ec 100644 --- a/capture/output/nuxt-dev-plain-static.txt +++ b/capture/output/nuxt-dev-plain-static.txt @@ -1,4 +1,4 @@ -styles: c1e3a506568cd274 +styles: 2956cfe899bf5ad9 press h + enter to see available shortcuts ➜ DevTools: press Shift + Alt + D in the browser (x.y.z) ➜ Local: http://localhost:3000/ @@ -7,7 +7,7 @@ styles: c1e3a506568cd274 ℹ Vite server warmed up in 42 ms │ │ config 42 ms · modules 42 ms · app 42 ms · types 42 ms · bundle 42 ms · server 42 ms -◆ Ready in 42 ms +◆ Ready in 42 ms → http://localhost:3000/ ● Nuxt x.y.z (with Nitro x.y.z, Vite x.y.z and Vue x.y.z) ✔ Nuxt Nitro server built in 42 ms nitro ✔ Vite client built in 42 ms diff --git a/capture/output/nuxt-dev-plain.svg b/capture/output/nuxt-dev-plain.svg index 5ae310109..aff8ce19c 100644 --- a/capture/output/nuxt-dev-plain.svg +++ b/capture/output/nuxt-dev-plain.svg @@ -16,24 +16,23 @@ svg{--bg:#ffffff;--fg:#24292f;--chrome:#f6f8fa;--dot:#d0d7de} nuxt dev (plain output) - - Starting Nuxt... 0.0s - Starting Nuxt... 0.1s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose Setting up modules 0.0s · 0.3s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Preparing app 0.0s · 0.4s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Preparing app 0.1s · 0.6s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Generating types 0.0s · 0.7s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Generating types 0.1s · 0.8s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Bundling app 0.0s · 1.0s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 44ms Vite server built in 21ms Building server 0.0s · 1.1s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 44ms Vite server built in 21ms Building server 0.0s · 1.1s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 44ms Vite server built in 21ms Building server 0.1s · 1.2s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 44ms Vite server built in 21ms Building server 0.2s · 1.3s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 44ms Vite server built in 21ms Building server 0.3s · 1.4s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 44ms Vite server built in 21ms Building server 0.4s · 1.5s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 44ms Vite server built in 21ms Building server 0.5s · 1.6s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 44ms Vite server built in 21ms Building server 0.5s · 1.7s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 44ms Vite server built in 21ms Building server 0.7s · 1.8s -Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 44ms Vite server built in 21ms Nuxt Nitro server built in 821ms nitro Ready in 2.06sconfig 257ms · modules 167ms · app 297ms · types 242ms · bundle 166ms · server 929mspressh + enterto see available shortcuts Vite server warmed up in 6ms Vite client warmed up in 11ms + + Starting Nuxt... 0.0s + Starting Nuxt... 0.1s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose Setting up modules 0.0s · 0.2s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Preparing app 0.0s · 0.4s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Preparing app 0.1s · 0.5s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Generating types 0.0s · 0.6s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Generating types 0.1s · 0.7s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Bundling app 0.0s · 0.9s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 42ms Vite server built in 21ms Building server 0.0s · 1.0s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 42ms Vite server built in 21ms Building server 0.1s · 1.1s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 42ms Vite server built in 21ms Building server 0.2s · 1.2s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 42ms Vite server built in 21ms Building server 0.3s · 1.3s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 42ms Vite server built in 21ms Building server 0.4s · 1.4s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 42ms Vite server built in 21ms Building server 0.5s · 1.5s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 42ms Vite server built in 21ms Building server 0.5s · 1.6s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 42ms Vite server built in 21ms Building server 0.7s · 1.7s +Nuxt 4.5.2 (with Nitro 2.13.4, Vite 8.2.2 and Vue 3.5.41)Local: http://localhost:3000/Network: use --host to expose ➜ DevTools: press Shift + Alt + D in the browser (v3.4.2) Re-optimizing dependencies because vite config has changed Vite client built in 42ms Vite server built in 21ms Nuxt Nitro server built in 813ms nitro Ready in 1.94s http://localhost:3000/config 234ms · modules 155ms · app 256ms · types 222ms · bundle 157ms · server 917mspressh + enterto see available shortcuts Vite server warmed up in 6ms Vite client warmed up in 11ms diff --git a/capture/output/nuxt-dev-plain.txt b/capture/output/nuxt-dev-plain.txt index 87494d75b..6cc44e2c8 100644 --- a/capture/output/nuxt-dev-plain.txt +++ b/capture/output/nuxt-dev-plain.txt @@ -1,4 +1,4 @@ -styles: 42fd7849ab5818cc +styles: 7e255c9f91b0d90b press h + enter to see available shortcuts ➜ DevTools: press Shift + Alt + D in the browser (x.y.z) ➜ Local: http://localhost:3000/ @@ -8,7 +8,7 @@ styles: 42fd7849ab5818cc ℹ Vite server warmed up in 42 ms │ │ config 42 ms · modules 42 ms · app 42 ms · types 42 ms · bundle 42 ms · server 42 ms -◆ Ready in 42 ms +◆ Ready in 42 ms → http://localhost:3000/ ● Nuxt x.y.z (with Nitro x.y.z, Vite x.y.z and Vue x.y.z) ✔ Nuxt Nitro server built in 42 ms nitro ✔ Vite client built in 42 ms diff --git a/packages/nuxt-cli/src/utils/phase-reporter.ts b/packages/nuxt-cli/src/utils/phase-reporter.ts index 62e79c485..812b72bb2 100644 --- a/packages/nuxt-cli/src/utils/phase-reporter.ts +++ b/packages/nuxt-cli/src/utils/phase-reporter.ts @@ -208,8 +208,9 @@ export function createPhaseReporter(options: PhaseReporterOptions = {}): PhaseRe animate() /** - * Repeat the phase in flight, with the time it has taken so far, so a long - * silent stretch in a piped log still shows the build is alive. + * Repeat whatever is in flight, with the time it has taken so far, so a long + * silent stretch in a piped log still shows something is alive. A render is + * repeated the same way: nothing else is printed while it compiles. */ function schedulePulse() { clearInterval(pulse) @@ -218,13 +219,24 @@ export function createPhaseReporter(options: PhaseReporterOptions = {}): PhaseRe return } pulse = setInterval(() => { - if (snapshot && !stopped) { - log(`${snapshot.message} ${styleText('dim', `(${formatElapsed(snapshot, Date.now() - receivedAt)})`)}`, snapshot.message) + if (!snapshot || stopped) { + return + } + const pending = summarised ? snapshot.pending : undefined + if (pending) { + log(announce(pending.label, Date.now() - pending.startedAt), `rendering ${pending.label}`) + return } + log(`${snapshot.message} ${styleText('dim', `(${formatElapsed(snapshot, Date.now() - receivedAt)})`)}`, snapshot.message) }, options.heartbeat) pulse.unref?.() } + /** A render, as its own line, for output that cannot redraw one in place. */ + function announce(label: string, elapsed?: number): string { + return `Rendering ${label}${elapsed === undefined ? '' : ` ${styleText('dim', `(${formatTicking(elapsed)})`)}`}` + } + /** Take the line down, leaving the terminal as it was found. */ function restore() { clearInterval(pulse) @@ -326,10 +338,12 @@ export function createPhaseReporter(options: PhaseReporterOptions = {}): PhaseRe render() return } - // A pipe cannot redraw, so each request is announced once as it starts. + // A pipe cannot redraw, so the request is announced as it starts and then + // repeated on the heartbeat, which is all that says the wait is moving. if (narrating !== next.pending.label) { narrating = next.pending.label - logger.info(`Rendering ${next.pending.label}`) + log(announce(next.pending.label), `rendering ${next.pending.label}`) + schedulePulse() } }, stop() { diff --git a/packages/nuxt-cli/test/unit/phase-reporter.spec.ts b/packages/nuxt-cli/test/unit/phase-reporter.spec.ts index 5abbe7175..34abb762d 100644 --- a/packages/nuxt-cli/test/unit/phase-reporter.spec.ts +++ b/packages/nuxt-cli/test/unit/phase-reporter.spec.ts @@ -282,6 +282,19 @@ describe('phase reporter', () => { expect(screen(renderer).match(/Ready in/g)).toHaveLength(1) }) + it('should repeat a render that has not landed, where the line cannot be redrawn', async () => { + const renderer = await render(async ({ waitForOutput }) => { + const startup = createPhaseReporter({ animated: false, heartbeat: 20 }) + reporters.push(startup) + const ready = { status: 'ready' as const, phase: 'ready', message: 'Ready', index: 6, elapsed: 2400 } + startup.update(snapshot(ready)) + startup.update(snapshot({ ...ready, pending: { label: 'GET /', startedAt: Date.now() - 12_500 } })) + await waitForOutput(/Rendering GET \/[\s\S]*Rendering GET \/[\s\S]*\(12\.\ds\)/) + }) + + expect(screen(renderer)).toContain('Rendering GET /') + }) + it('should not close off a wait it never reported', async () => { const renderer = await render(() => { const startup = reporter(false) From bc4726b4850d14924ccefbf21d1ae7c49650365d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 25 Aug 2026 19:02:22 +0000 Subject: [PATCH 5/7] test(dev): expect the loading page to wait for the first render --- capture/output/nuxt-init.svg | 21 +++++++-------------- packages/nuxt-cli/test/e2e/dev.spec.ts | 26 +++++++++++++++++++------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/capture/output/nuxt-init.svg b/capture/output/nuxt-init.svg index a44abc7a1..a318e5af6 100644 --- a/capture/output/nuxt-init.svg +++ b/capture/output/nuxt-init.svg @@ -16,19 +16,12 @@ svg{--bg:#ffffff;--fg:#24292f;--chrome:#f6f8fa;--dot:#d0d7de} npm create nuxt - .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! - .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Loading available templates - .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Loading available templates - .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Loading available templates - .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! - .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Loading available templates - .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Loading available templates - .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Templates loaded Which template would you like to use?content · Starter for a content-driven website.minimal · Minimal starter with a single app.vue. (recommended)module · Starter to create your first Nuxt module.ui · Starter with Nuxt UI.v5-nightly · Minimal setup for Nuxt 5 Nightly↑/↓ to navigate • Enter: confirm - .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Templates loaded Which template would you like to use?minimal · Minimal starter with a single app.vue. Creating project in my-app - .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Templates loaded Which template would you like to use?minimal · Minimal starter with a single app.vue. Creating project in my-app Downloading minimal template - .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Templates loaded Which template would you like to use?minimal · Minimal starter with a single app.vue. Creating project in my-app Downloading minimal template - .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Templates loaded Which template would you like to use?minimal · Minimal starter with a single app.vue. Creating project in my-app Downloading minimal template - .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Templates loaded Which template would you like to use?minimal · Minimal starter with a single app.vue. Creating project in my-app Downloaded minimal template Skipping install dependencies step. Would you like to browse and install modules?Yes/ No - Creating project in my-app Downloaded minimal template Skipping install dependencies step. Would you like to browse and install modules?No Created your project from the minimal template to scaffold this project again without prompts:npm create nuxt@latest my-app -- --template=minimal --packageManager=npm --no-gitInit \ --no-modules --no-install Next steps:cd my-appnpm installnpm run dev ✨ Happy building! + .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P` + .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! + .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Loading available templates + .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Templates loaded Which template would you like to use?content · Starter for a content-driven website.minimal · Minimal starter with a single app.vue. (recommended)module · Starter to create your first Nuxt module.ui · Starter with Nuxt UI.v5-nightly · Minimal setup for Nuxt 5 Nightly↑/↓ to navigate • Enter: confirm + .d$b. i$$A$$L .d$b .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Templates loaded Which template would you like to use?minimal · Minimal starter with a single app.vue. Creating project in my-app + .$$F` `$$L.$$A$$. j$$' `4$$:` `$$. j$$' .4$: `$$. j$$` .$$: `4$L :$$:____.d$$: _____.:$$: `4$$$$$$$$P` .i$$$$$$$$P`Welcome to Nuxt! Templates loaded Which template would you like to use?minimal · Minimal starter with a single app.vue. Creating project in my-app Downloaded minimal template Skipping install dependencies step. Would you like to browse and install modules?Yes/ No + Creating project in my-app Downloaded minimal template Skipping install dependencies step. Would you like to browse and install modules?No Created your project from the minimal template to scaffold this project again without prompts:npm create nuxt@latest my-app -- --template=minimal --packageManager=npm --no-gitInit \ --no-modules --no-install Next steps:cd my-appnpm installnpm run dev ✨ Happy building! diff --git a/packages/nuxt-cli/test/e2e/dev.spec.ts b/packages/nuxt-cli/test/e2e/dev.spec.ts index dbe334a62..9ba867163 100644 --- a/packages/nuxt-cli/test/e2e/dev.spec.ts +++ b/packages/nuxt-cli/test/e2e/dev.spec.ts @@ -139,17 +139,29 @@ describe('dev server', () => { const reader = response.body!.getReader() const decoder = new TextDecoder() let stream = '' - while (!stream.includes('event: nuxt:ready')) { - const { value, done } = await reader.read() - if (done) { - break + async function readUntil(marker: string): Promise { + while (!stream.includes(marker)) { + const { value, done } = await reader.read() + if (done) { + return + } + stream += decoder.decode(value) } - stream += decoder.decode(value) } - await reader.cancel() + // Only whole events, so a half-read chunk cannot be parsed as a snapshot. + const latest = () => JSON.parse([...stream.matchAll(/data: (.+)\n/g)].pop()![1]!) + await readUntil('event: nuxt:ready') expect(stream).toContain('event: nuxt:ready') - expect(JSON.parse(stream.split('data: ').pop()!)).toMatchObject({ status: 'ready', progress: 1 }) + // Whoever is streaming this is waiting for a page, and being ready means + // the server can accept that request rather than that it has answered it. + expect(latest()).toMatchObject({ status: 'ready', serving: false }) + + await fetch(`http://${host}:${port}/`, { headers: { accept: 'text/html' } }) + await readUntil('"serving":true') + await reader.cancel() + + expect(latest()).toMatchObject({ status: 'ready', serving: true, progress: 1 }) } finally { await close() From feec1f31fd3b1f7cfba2dd964b52a20ef18cf6b3 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 25 Aug 2026 19:14:59 +0000 Subject: [PATCH 6/7] fix(dev): drop a stale render on handover + warm up a fork's first render --- packages/nuxt-cli/src/commands/dev.ts | 5 ++- packages/nuxt-cli/src/dev/index.ts | 6 ++-- packages/nuxt-cli/src/dev/tui/controller.ts | 8 +++-- packages/nuxt-cli/src/dev/tui/index.ts | 3 +- packages/nuxt-cli/src/dev/tui/session.ts | 3 ++ packages/nuxt-cli/src/dev/utils.ts | 2 +- packages/nuxt-cli/test/unit/dev-tui.spec.ts | 34 +++++++++++++++++++++ 7 files changed, 54 insertions(+), 7 deletions(-) diff --git a/packages/nuxt-cli/src/commands/dev.ts b/packages/nuxt-cli/src/commands/dev.ts index 988a51c86..b39f97044 100644 --- a/packages/nuxt-cli/src/commands/dev.ts +++ b/packages/nuxt-cli/src/commands/dev.ts @@ -313,6 +313,9 @@ const command = defineCommand({ const explanation = formatRestartReason(reason, { rootDir: cwd, hard: true }) logger.info(explanation) devUI.setStatus('restarting', explanation) + // The process that was rendering is about to be replaced, and it may not + // live long enough to say that its request has gone. + devUI.setRendering(undefined) // The inspector port cannot be shared, so the handover has to stay // serialised whenever the inspector is open. @@ -352,7 +355,7 @@ const command = defineCommand({ devUI.setStatus(message.building ? 'building' : 'ready') } else if (message.type === 'nuxt:internal:dev:rendering') { - devUI.setRendering(message.pending) + devUI.setRendering(message.pending, message.awaiting) } else if (message.type === 'nuxt:internal:dev:ready' || message.type === 'nuxt:internal:dev:loading:error') { serving = true diff --git a/packages/nuxt-cli/src/dev/index.ts b/packages/nuxt-cli/src/dev/index.ts index bfbcf1915..bbcf8c37f 100644 --- a/packages/nuxt-cli/src/dev/index.ts +++ b/packages/nuxt-cli/src/dev/index.ts @@ -332,13 +332,15 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti // fork has to report between being ready and having answered. if (ipc.enabled) { let reported: string | undefined - devServer.progress.onUpdate(({ status, pending }) => { + devServer.progress.onUpdate(({ status, pending, serving }) => { const rendering = status === 'ready' ? pending : undefined if (rendering?.label === reported) { return } reported = rendering?.label - ipc.send({ type: 'nuxt:internal:dev:rendering', pending: rendering }) + // Whether this is the render everything is waiting for is the fork's to + // know: the parent only sees that a request is in flight. + ipc.send({ type: 'nuxt:internal:dev:rendering', pending: rendering, awaiting: !serving }) }) } diff --git a/packages/nuxt-cli/src/dev/tui/controller.ts b/packages/nuxt-cli/src/dev/tui/controller.ts index 603e214ee..78729ac70 100644 --- a/packages/nuxt-cli/src/dev/tui/controller.ts +++ b/packages/nuxt-cli/src/dev/tui/controller.ts @@ -19,8 +19,12 @@ export interface DevUIController { pushRequests: (requests: DevRequestEvent[]) => void /** Replace the routes shown in the route view. */ setRoutes: (routes: DevRoutes) => void - /** Report the render the server is busy with, or that it is busy with none. */ - setRendering: (pending?: PendingRender) => void + /** + * Report the render the server is busy with, or that it is busy with none. + * `awaiting` says no page has been rendered yet, which is what makes it a + * warmup rather than one request among many. + */ + setRendering: (pending?: PendingRender, awaiting?: boolean) => void } /** What the plain fallback answers to everything the UI would have shown. */ diff --git a/packages/nuxt-cli/src/dev/tui/index.ts b/packages/nuxt-cli/src/dev/tui/index.ts index 1ec1eb6ab..2d113f3af 100644 --- a/packages/nuxt-cli/src/dev/tui/index.ts +++ b/packages/nuxt-cli/src/dev/tui/index.ts @@ -595,10 +595,11 @@ export function setupDevUI(context: ShortcutContext, options: DevUIOptions = {}) activityTimer.unref?.() }, setRoutes: payload => routeOverlay.setRoutes(payload), - setRendering: (pending) => { + setRendering: (pending, awaiting) => { update({ rendering: pending && { label: pending.label, startedAt: pending.startedAt }, renderingMs: pending && Date.now() - pending.startedAt, + ...awaiting === undefined ? {} : { awaitingFirstRender: awaiting }, }) }, } diff --git a/packages/nuxt-cli/src/dev/tui/session.ts b/packages/nuxt-cli/src/dev/tui/session.ts index 737e421c0..36265e18e 100644 --- a/packages/nuxt-cli/src/dev/tui/session.ts +++ b/packages/nuxt-cli/src/dev/tui/session.ts @@ -221,6 +221,9 @@ export function beginDevUI(options: DevUISupportOptions & { version?: string, cw Object.assign(state, { status: snapshot.reload ? 'building' : 'starting', note: snapshot.message, + // A load starting voids whatever was being rendered against the last one. + rendering: undefined, + renderingMs: undefined, loadStartedAt: Date.now() - snapshot.elapsed, elapsedMs: snapshot.elapsed, phaseStartedAt: Date.now() - snapshot.phaseElapsed, diff --git a/packages/nuxt-cli/src/dev/utils.ts b/packages/nuxt-cli/src/dev/utils.ts index b30e80fb0..6fec86ebc 100644 --- a/packages/nuxt-cli/src/dev/utils.ts +++ b/packages/nuxt-cli/src/dev/utils.ts @@ -96,7 +96,7 @@ export type NuxtDevIPCMessage | { type: 'nuxt:internal:dev:requests', requests: DevRequestEvent[] } | { type: 'nuxt:internal:dev:routes', payload: DevRoutes } | { type: 'nuxt:internal:dev:building', building: boolean } - | { type: 'nuxt:internal:dev:rendering', pending?: PendingRender } + | { type: 'nuxt:internal:dev:rendering', pending?: PendingRender, awaiting?: boolean } export interface NuxtDevContext { cwd: string diff --git a/packages/nuxt-cli/test/unit/dev-tui.spec.ts b/packages/nuxt-cli/test/unit/dev-tui.spec.ts index 53d64a4c5..868edaeaa 100644 --- a/packages/nuxt-cli/test/unit/dev-tui.spec.ts +++ b/packages/nuxt-cli/test/unit/dev-tui.spec.ts @@ -2093,6 +2093,29 @@ describe('dev ui teardown', () => { } } + it('drops a render in flight when a new load starts', async () => { + await withTerminal(({ session }) => { + const ready = { + status: 'ready' as const, + phase: 'ready', + message: 'Ready', + index: 6, + total: 6, + progress: 0.95, + elapsed: 2400, + phaseElapsed: 0, + reload: false, + serving: false, + timings: [], + } + session.reportProgress({ ...ready, pending: { label: 'GET /', startedAt: Date.now() } }) + expect(session.state.rendering?.label).toBe('GET /') + + session.reportProgress({ ...ready, status: 'loading', phase: 'config', message: 'Reloading Nuxt...', reload: true }) + expect(session.state.rendering).toBeUndefined() + }) + }) + it('surfaces errors still waiting on their delay when it tears down', async () => { await withTerminal(({ session, written }) => { session.events.push({ time: Date.now(), level: 0, type: 'error', message: 'the server could not start', source: 'cli' }) @@ -2341,6 +2364,17 @@ describe('request failures on the panel', () => { }) }) + it('should show a fork\'s first render as a warmup', async () => { + await withPanel(async (ui, settle) => { + ui.setStatus('ready') + ui.setRendering({ label: 'GET /', startedAt: Date.now() }, true) + expect(await settle()).toContain('WARMUP') + + ui.setRendering({ label: 'GET /about', startedAt: Date.now() }, false) + expect(await settle()).toContain('READY') + }) + }) + it('should keep reporting a render when the server reports itself ready again', async () => { await withPanel(async (ui, settle) => { ui.setStatus('ready') From c43c6d4d9db057d266ad871943463d3b4b2dc084 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 25 Aug 2026 19:14:59 +0000 Subject: [PATCH 7/7] test(dev): freeze the clock where a render duration is asserted --- packages/nuxt-cli/test/unit/phase-reporter.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nuxt-cli/test/unit/phase-reporter.spec.ts b/packages/nuxt-cli/test/unit/phase-reporter.spec.ts index 34abb762d..34225f0f9 100644 --- a/packages/nuxt-cli/test/unit/phase-reporter.spec.ts +++ b/packages/nuxt-cli/test/unit/phase-reporter.spec.ts @@ -250,6 +250,7 @@ describe('phase reporter', () => { }) it('should announce a render once where the line cannot be redrawn', async () => { + freezeClock() const renderer = await render(() => { const startup = reporter(false) startup.update(snapshot({ status: 'ready', phase: 'ready', message: 'Ready', index: 6, elapsed: 2400 }))