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.02s│config 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.02s│config 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.02s│config 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.02s│config 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.02s│config 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.02s│config 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.02s│config 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.03s│config 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.06s│config 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/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-app│ › npm install│ › npm 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-app│ › npm install│ › npm run dev│└ ✨ Happy building!
diff --git a/packages/nuxt-cli/src/commands/dev.ts b/packages/nuxt-cli/src/commands/dev.ts
index f67254e99..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.
@@ -351,6 +354,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, message.awaiting)
+ }
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 3558078be..bbcf8c37f 100644
--- a/packages/nuxt-cli/src/dev/index.ts
+++ b/packages/nuxt-cli/src/dev/index.ts
@@ -328,6 +328,22 @@ 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, serving }) => {
+ const rendering = status === 'ready' ? pending : undefined
+ if (rendering?.label === reported) {
+ return
+ }
+ reported = rendering?.label
+ // 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 })
+ })
+ }
+
// 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[] = []
@@ -366,6 +382,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 +397,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/controller.ts b/packages/nuxt-cli/src/dev/tui/controller.ts
index 3edbf388a..78729ac70 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,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.
+ * `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. */
@@ -27,6 +34,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..2d113f3af 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,
@@ -582,6 +595,13 @@ export function setupDevUI(context: ShortcutContext, options: DevUIOptions = {})
activityTimer.unref?.()
},
setRoutes: payload => routeOverlay.setRoutes(payload),
+ 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/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 2e21be6b9..36265e18e 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,19 +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.
+ // 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 = snapshot.serving ? undefined : snapshot.message
- state.progress = snapshot.serving ? undefined : snapshot.progress
+ 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 = snapshot.serving ? 'ready' : 'warming'
- render()
+ state.status = waiting ? 'warming' : 'ready'
}
+ repaint()
return
}
if (snapshot.status !== 'loading') {
@@ -197,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,
@@ -410,6 +437,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/src/dev/utils.ts b/packages/nuxt-cli/src/dev/utils.ts
index 800e46fa3..6fec86ebc 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, awaiting?: boolean }
export interface NuxtDevContext {
cwd: string
@@ -546,8 +548,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..812b72bb2 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,15 +195,22 @@ 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.
+ * 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)
@@ -175,13 +219,25 @@ 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)
pulse = undefined
@@ -189,70 +245,109 @@ 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 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
+ log(announce(next.pending.label), `rendering ${next.pending.label}`)
+ schedulePulse()
}
},
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/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()
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..868edaeaa 100644
--- a/packages/nuxt-cli/test/unit/dev-tui.spec.ts
+++ b/packages/nuxt-cli/test/unit/dev-tui.spec.ts
@@ -74,6 +74,40 @@ describe('dev tui panel', () => {
expect(lines.join('\n')).not.toContain('READY')
})
+ 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 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', () => {
for (const status of ['building', 'restarting', 'error'] as const) {
expect(strip(renderPanel({ ...READY, status }, 100, 30)[0]!)).not.toContain('ready in')
@@ -2059,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' })
@@ -2287,6 +2344,50 @@ 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 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')
+ 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')
diff --git a/packages/nuxt-cli/test/unit/phase-reporter.spec.ts b/packages/nuxt-cli/test/unit/phase-reporter.spec.ts
index a784a1f01..34225f0f9 100644
--- a/packages/nuxt-cli/test/unit/phase-reporter.spec.ts
+++ b/packages/nuxt-cli/test/unit/phase-reporter.spec.ts
@@ -216,6 +216,96 @@ 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 () => {
+ freezeClock()
+ 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 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)
+ 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)