Skip to content

feat(nuxi): add nuxt doctor diagnostic command#1206

Open
onmax wants to merge 1 commit intomainfrom
feat/doctor
Open

feat(nuxi): add nuxt doctor diagnostic command#1206
onmax wants to merge 1 commit intomainfrom
feat/doctor

Conversation

@onmax
Copy link

@onmax onmax commented Jan 31, 2026

Closes #1205

Hook-based diagnostic command. Core checks built-in, modules contribute via doctor:check hook.

image

CLI Flags

Flag Behavior
--verbose -v Show details, suggestions, URLs verbose
--json Output full array as JSON json

Module integration

nuxt.hook('doctor:check', (ctx) => {
  ctx.addCheck({
    id: 'MY_CHECK',
    name: 'MyModule',
    status: 'warning',
    message: 'issue found',
    source: 'my-module',
    suggestion: 'Fix by doing X',
    url: 'https://docs.example.com'
  })
})

Demo

nuxt-doctor-showcase

git clone https://github.com/onmax/nuxt-doctor-showcase
cd nuxt-doctor-showcase
pnpm install
npx nuxi doctor
npx nuxi doctor --verbose
npx nuxi doctor --json | jq
Output (normal)
┌  Running diagnostics...
│
│  [✓] Versions - Node v24.12.0, Nuxt 4.3.0
│
│  [!] Config - 1 issue found
│      → missing "compatibilityDate" - add to nuxt.config.ts
│
│  [✓] Modules - 2 modules loaded
│
│  [✗] Analytics (via analytics) - missing required config
│      → trackingId is required but not set
│
└  Diagnostics complete with errors
Output (--verbose)
┌  Running diagnostics...
│
│  [✓] Versions - Node v24.12.0, Nuxt 4.3.0
│
│  [!] Config - 1 issue found
│      → missing "compatibilityDate" - add to nuxt.config.ts
│      💡 Review nuxt.config.ts and fix the issues above
│      🔗 https://nuxt.com/docs/getting-started/configuration
│
│  [✓] Modules - 2 modules loaded
│
│  [✗] Analytics (via analytics) - missing required config
│      → trackingId is required but not set
│      💡 Add analytics: { trackingId: "UA-XXXXX-Y" } to nuxt.config.ts
│      🔗 https://analytics.google.com/
│
└  Diagnostics complete with errors
Output (--json)
[
  {
    "name": "Versions",
    "status": "success",
    "message": "Node v24.12.0, Nuxt 4.3.0"
  },
  {
    "name": "Config",
    "status": "warning",
    "message": "1 issue found",
    "details": ["missing \"compatibilityDate\" - add to nuxt.config.ts"],
    "suggestion": "Review nuxt.config.ts and fix the issues above",
    "url": "https://nuxt.com/docs/getting-started/configuration"
  },
  {
    "name": "Modules",
    "status": "success",
    "message": "2 modules loaded"
  },
  {
    "id": "ANALYTICS_MISSING_ID",
    "name": "Analytics",
    "status": "error",
    "message": "missing required config",
    "source": "analytics",
    "details": ["trackingId is required but not set"],
    "suggestion": "Add analytics: { trackingId: \"UA-XXXXX-Y\" } to nuxt.config.ts",
    "url": "https://analytics.google.com/"
  }
]

Related

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 31, 2026

  • nuxt-cli-playground

    npm i https://pkg.pr.new/create-nuxt@1206
    
    npm i https://pkg.pr.new/nuxi@1206
    
    npm i https://pkg.pr.new/@nuxt/cli@1206
    

commit: ec8a097

@github-actions
Copy link
Contributor

github-actions bot commented Jan 31, 2026

📦 Bundle Size Comparison

📈 nuxi

Metric Base Head Diff
Rendered 3708.86 KB 3716.52 KB +7.66 KB (+0.21%)

📈 nuxt-cli

Metric Base Head Diff
Rendered 137.36 KB 144.91 KB +7.55 KB (+5.50%)

➡️ create-nuxt

Metric Base Head Diff
Rendered 1656.98 KB 1656.98 KB 0.00 KB (0.00%)

@codspeed-hq
Copy link

codspeed-hq bot commented Jan 31, 2026

CodSpeed Performance Report

Merging this PR will not alter performance

Comparing feat/doctor (ec8a097) with main (fae912b)

Summary

✅ 2 untouched benchmarks

@onmax onmax force-pushed the feat/doctor branch 3 times, most recently from 3868c71 to 308dce5 Compare February 1, 2026 08:58
@onmax onmax marked this pull request as ready for review February 1, 2026 09:29
@onmax onmax requested a review from danielroe as a code owner February 1, 2026 09:29
@coderabbitai
Copy link

coderabbitai bot commented Feb 1, 2026

📝 Walkthrough

Walkthrough

Adds a new Nuxt CLI command "doctor" (packages/nuxi/src/commands/doctor.ts) that runs diagnostic checks (versions, config, modules), defines DoctorCheck and DoctorCheckContext types, and augments @nuxt/schema NuxtHooks with doctor:check so modules can contribute checks. The command resolves and bootstraps Nuxt, runs core checks, emits the doctor:check hook for module contributions, and renders results in human-readable or JSON form (with verbose mode). It is registered in the commands index and includes unit and e2e tests covering success, error, JSON/verbose outputs, Node-version gating, and lifecycle cleanup.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(nuxi): add nuxt doctor diagnostic command' clearly and concisely summarizes the main feature addition.
Description check ✅ Passed The description is directly related to the changeset, explaining the hook-based doctor command with CLI flags, module integration examples, and demo outputs.
Linked Issues check ✅ Passed The PR implements all core objectives from issue #1205: hook-based doctor command with core checks (versions, config, modules), doctor:check hook for modules, and CLI flags for verbose/JSON output.
Out of Scope Changes check ✅ Passed All changes are within scope: the new doctor command, its tests, and command index entry directly support the stated objectives without introducing unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/doctor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@packages/nuxi/test/unit/commands/doctor.spec.ts`:
- Around line 295-303: Guard against undefined when reading the console spy
output: ensure consoleSpy.mock.calls[0] and its first element exist before
passing to JSON.parse. In the test around the consoleSpy usage, change the
rawOutput assignment to safely access the value (e.g., use optional chaining or
a fallback like const rawOutput = consoleSpy.mock.calls[0]?.[0] ?? '') and only
JSON.parse when rawOutput is non-empty; reference the consoleSpy variable and
the rawOutput/JSON.parse usage to locate where to apply the guard, then keep the
consoleSpy.mockRestore() call as-is.
- Around line 332-351: The test assumes consoleSpy.mock.calls[0][0] and the
found testCheck are always defined; guard against undefined to satisfy
TypeScript by checking the mock call exists (consoleSpy.mock.calls.length > 0
and consoleSpy.mock.calls[0]?.[0]) before JSON.parse and asserting testCheck is
defined (or using a non-null assertion/expect(testCheck).toBeDefined()) before
accessing its properties; update the references around the output and testCheck
variables (consoleSpy, output, testCheck) accordingly so the subsequent
expect(...) property checks are type-safe.
- Around line 270-280: The test accesses consoleSpy.mock.calls[0][0] without
guarding for undefined, causing a TypeScript error; before reading the call
value in the test (where consoleSpy is used and rawOutput is assigned), add an
assertion that a call exists (e.g. expect(consoleSpy).toHaveBeenCalled() or
expect(consoleSpy.mock.calls.length).toBeGreaterThan(0)) so
consoleSpy.mock.calls[0] is guaranteed defined, then proceed to assign rawOutput
and parse it; update references to consoleSpy and rawOutput in the same block to
rely on that assertion.
🧹 Nitpick comments (2)
packages/nuxi/src/commands/doctor.ts (1)

243-247: Silent early return may confuse users.

When nuxtVersion is undefined, the function returns without adding any check result. This leaves "Modules" absent from the output, which could be confusing. Consider adding a warning or informational check.

💡 Suggested improvement
 async function checkModuleCompat(checks: DoctorCheck[], nuxt: Nuxt, cwd: string): Promise<void> {
   const nuxtVersion = await resolveNuxtVersion(cwd)
   if (!nuxtVersion) {
+    checks.push({
+      name: 'Modules',
+      status: 'warning',
+      message: 'could not determine Nuxt version for compatibility check',
+    })
     return
   }
packages/nuxi/test/unit/commands/doctor.spec.ts (1)

161-190: Restore process.version in afterEach to avoid test pollution.

If the test fails before line 189, process.version remains modified for subsequent tests. Use afterEach for cleanup to ensure restoration regardless of test outcome.

♻️ Suggested fix

Add an afterEach hook at the top level of the describe block:

afterEach(() => {
  Object.defineProperty(process, 'version', { value: originalProcessVersion, configurable: true })
})

Then remove line 189 from the test body.

Comment on lines 295 to 310

expect(consoleSpy).toHaveBeenCalledOnce()
const rawOutput = consoleSpy.mock.calls[0][0]
expect(() => JSON.parse(rawOutput)).not.toThrow()
const output = JSON.parse(rawOutput)
expect(output[0].status).toBe('error')
expect(output[0].message).toContain('Failed to load Nuxt')

consoleSpy.mockRestore()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against undefined to fix TypeScript error.

Same issue as line 272.

🔧 Proposed fix
     expect(consoleSpy).toHaveBeenCalledOnce()
-    const rawOutput = consoleSpy.mock.calls[0][0]
+    const rawOutput = consoleSpy.mock.calls[0]?.[0]
+    expect(rawOutput).toBeDefined()
     expect(() => JSON.parse(rawOutput)).not.toThrow()
📝 Committable suggestion

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

Suggested change
expect(consoleSpy).toHaveBeenCalledOnce()
const rawOutput = consoleSpy.mock.calls[0][0]
expect(() => JSON.parse(rawOutput)).not.toThrow()
const output = JSON.parse(rawOutput)
expect(output[0].status).toBe('error')
expect(output[0].message).toContain('Failed to load Nuxt')
consoleSpy.mockRestore()
expect(consoleSpy).toHaveBeenCalledOnce()
const rawOutput = consoleSpy.mock.calls[0]?.[0]
expect(rawOutput).toBeDefined()
expect(() => JSON.parse(rawOutput)).not.toThrow()
const output = JSON.parse(rawOutput)
expect(output[0].status).toBe('error')
expect(output[0].message).toContain('Failed to load Nuxt')
consoleSpy.mockRestore()
🧰 Tools
🪛 GitHub Check: ci (macos-latest)

[failure] 297-297:
Object is possibly 'undefined'.

🪛 GitHub Check: ci (ubuntu-latest)

[failure] 297-297:
Object is possibly 'undefined'.

🪛 GitHub Check: ci (windows-latest)

[failure] 297-297:
Object is possibly 'undefined'.

🤖 Prompt for AI Agents
In `@packages/nuxi/test/unit/commands/doctor.spec.ts` around lines 295 - 303,
Guard against undefined when reading the console spy output: ensure
consoleSpy.mock.calls[0] and its first element exist before passing to
JSON.parse. In the test around the consoleSpy usage, change the rawOutput
assignment to safely access the value (e.g., use optional chaining or a fallback
like const rawOutput = consoleSpy.mock.calls[0]?.[0] ?? '') and only JSON.parse
when rawOutput is non-empty; reference the consoleSpy variable and the
rawOutput/JSON.parse usage to locate where to apply the guard, then keep the
consoleSpy.mockRestore() call as-is.

@onmax onmax force-pushed the feat/doctor branch 2 times, most recently from 37ff846 to 6c142ed Compare February 1, 2026 10:06
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/nuxi/test/unit/commands/doctor.spec.ts`:
- Around line 161-190: The test is mocking process.version but the code under
test checks process.versions.node; update the test "should error on Node.js <
18" to set process.versions.node to '16.20.0' (and keep it configurable) before
importing the doctor command and restore the original process.versions.node
after the test so the version check in doctor.run triggers and mockExit is
called; ensure you update the mocked property rather than process.version and
keep references to mockLoadNuxt, mockExit, and the imported doctor command
consistent.

@onmax onmax force-pushed the feat/doctor branch 2 times, most recently from 5f08226 to 771f133 Compare February 1, 2026 11:15
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/nuxi/src/commands/doctor.ts`:
- Around line 118-125: Wrap the nuxt.callHook('doctor:check', ...) invocation in
a try-catch so that thrown exceptions from module hooks do not abort the doctor
command; on catch, log a warning and continue. When handling addCheck in the
hook payload, validate incoming DoctorCheck objects before pushing into the
checks array: ensure required fields (e.g., id/name/message) exist and that
check.status is one of the expected enum values used by
displayResults/statusStyles; if invalid, skip the entry and emit a warning.
Ensure displayResults is only called with the validated checks array.

Comment on lines 118 to 125
// 2. Let modules contribute via hook
await nuxt.callHook('doctor:check', {
addCheck: (c: DoctorCheck) => checks.push(c),
nuxt,
})

// 3. Display results
displayResults(checks, { verbose: ctx.args.verbose, json: ctx.args.json })
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/nuxi/src/commands/doctor.ts | sed -n '115,130p'

Repository: nuxt/cli

Length of output: 568


🏁 Script executed:

# Find the displayResults function to understand how it handles checks
rg -A 20 "function displayResults|const displayResults|export.*displayResults" packages/nuxi/src/commands/doctor.ts

Repository: nuxt/cli

Length of output: 856


🏁 Script executed:

# Search for DoctorCheck type definition
rg -B 5 -A 10 "type DoctorCheck|interface DoctorCheck" packages/nuxi/src

Repository: nuxt/cli

Length of output: 1969


🏁 Script executed:

# Check if there's any error handling around the callHook in the current code
rg -B 5 -A 10 "doctor:check" packages/nuxi/src/commands/doctor.ts

Repository: nuxt/cli

Length of output: 939


🏁 Script executed:

# Search for callHook usage patterns elsewhere to understand exception behavior
rg "await.*callHook" packages/nuxi/src/commands/ -A 3 | head -40

Repository: nuxt/cli

Length of output: 771


🌐 Web query:

Nuxt hookable callHook exception handling error propagation

💡 Result:

In Nuxt, the hooks system is built on @unjs/hookable. In hookable v5+, error handling changed in an important way:

  • If any hook handler throws (or returns a rejected promise), callHook(...) rejects (it no longer “swallows” the error and logs / emits a global error hook like hookable < v5 did). This means errors now propagate to the caller and must be handled with await/try-catch (or .catch(...)). [1][2]

What this means in practice

1) Errors propagate to the callHook caller

try {
  await nuxtApp.callHook('my:hook', payload)
} catch (err) {
  // err came from one of the hook callbacks
}

If you do nuxtApp.callHook(...) without awaiting/handling the returned promise, you can end up with an unhandled promise rejection (because the promise is rejected when a hook throws). [1][2]

2) If you don’t want a hook to fail the whole hook call, catch inside the hook

nuxtApp.hook('my:hook', async () => {
  try {
    await doWork()
  } catch (e) {
    // handle/log, but don’t rethrow if you want to prevent propagation
  }
})

3) Nuxt-level error hooks are separate from hookable propagation
Nuxt documents framework hooks like:

  • app:error (startup/plugin/app init failures)
  • vue:error (top-level Vue rendering errors)
    These are for observing/reporting errors at the Nuxt/Vue layer, but they don’t change the fact that a thrown error in a hook handler can reject callHook and should be caught where you call it. [3]

Sources: hookable v5 breaking change on callHook rejection behavior [1][2]; Nuxt error hooks (app:error, vue:error) [3].


Guard module hook failures and validate contributed checks to prevent doctor from crashing.

In @unjs/hookable v5+, callHook() propagates exceptions from hook handlers to the caller. Without a try-catch, a thrown module hook will unhandled-reject and abort the command. Additionally, displayResults() accesses statusStyles[check.status] without validating the status value at runtime—a malformed check from a module can break output rendering.

Wrap the hook call in try-catch and validate payloads before pushing:

🔧 Suggested hardening
-      await nuxt.callHook('doctor:check', {
-        addCheck: (c: DoctorCheck) => checks.push(c),
-        nuxt,
-      })
+      const addCheck = (c: DoctorCheck) => {
+        const validStatus = c?.status === 'success' || c?.status === 'warning' || c?.status === 'error'
+        if (!c?.name || !c?.message || !validStatus) {
+          checks.push({
+            id: 'INVALID_DOCTOR_CHECK',
+            name: 'Doctor',
+            status: 'error',
+            message: 'Invalid doctor:check payload from module',
+            source: c?.source,
+            data: { received: c },
+          })
+          return
+        }
+        checks.push(c)
+      }
+
+      try {
+        await nuxt.callHook('doctor:check', { addCheck, nuxt })
+      }
+      catch (err) {
+        checks.push({
+          id: 'DOCTOR_HOOK_FAILED',
+          name: 'Doctor',
+          status: 'error',
+          message: `doctor:check hook failed: ${err instanceof Error ? err.message : String(err)}`,
+        })
+      }
📝 Committable suggestion

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

Suggested change
// 2. Let modules contribute via hook
await nuxt.callHook('doctor:check', {
addCheck: (c: DoctorCheck) => checks.push(c),
nuxt,
})
// 3. Display results
displayResults(checks, { verbose: ctx.args.verbose, json: ctx.args.json })
// 2. Let modules contribute via hook
const addCheck = (c: DoctorCheck) => {
const validStatus = c?.status === 'success' || c?.status === 'warning' || c?.status === 'error'
if (!c?.name || !c?.message || !validStatus) {
checks.push({
id: 'INVALID_DOCTOR_CHECK',
name: 'Doctor',
status: 'error',
message: 'Invalid doctor:check payload from module',
source: c?.source,
data: { received: c },
})
return
}
checks.push(c)
}
try {
await nuxt.callHook('doctor:check', { addCheck, nuxt })
}
catch (err) {
checks.push({
id: 'DOCTOR_HOOK_FAILED',
name: 'Doctor',
status: 'error',
message: `doctor:check hook failed: ${err instanceof Error ? err.message : String(err)}`,
})
}
// 3. Display results
displayResults(checks, { verbose: ctx.args.verbose, json: ctx.args.json })
🤖 Prompt for AI Agents
In `@packages/nuxi/src/commands/doctor.ts` around lines 118 - 125, Wrap the
nuxt.callHook('doctor:check', ...) invocation in a try-catch so that thrown
exceptions from module hooks do not abort the doctor command; on catch, log a
warning and continue. When handling addCheck in the hook payload, validate
incoming DoctorCheck objects before pushing into the checks array: ensure
required fields (e.g., id/name/message) exist and that check.status is one of
the expected enum values used by displayResults/statusStyles; if invalid, skip
the entry and emit a warning. Ensure displayResults is only called with the
validated checks array.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: nuxt doctor with hook-based architecture

1 participant