Skip to content

Commit 308dce5

Browse files
committed
feat(nuxi): add --verbose and --json flags to doctor
1 parent 517377c commit 308dce5

File tree

2 files changed

+299
-105
lines changed

2 files changed

+299
-105
lines changed

packages/nuxi/src/commands/doctor.ts

Lines changed: 106 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,22 @@ import { cwdArgs, legacyRootDirArgs, logLevelArgs } from './_shared'
1515

1616
// Types for the hook-based architecture (will move to @nuxt/schema)
1717
interface DoctorCheck {
18+
// Required
1819
name: string
1920
status: 'success' | 'warning' | 'error'
2021
message: string
21-
source?: string // module name, e.g. "@nuxt/a11y"
22-
details?: string[] // list of issues/details to display
22+
23+
// Optional - identity/origin
24+
id?: string // programmatic code: "MISSING_PEER_DEP"
25+
source?: string // module name: "@nuxt/ui"
26+
27+
// Optional - verbose fields
28+
details?: string | string[]
29+
suggestion?: string
30+
url?: string
31+
32+
// Optional - programmatic
33+
data?: Record<string, unknown>
2334
}
2435

2536
interface DoctorCheckContext {
@@ -34,12 +45,20 @@ declare module '@nuxt/schema' {
3445
}
3546
}
3647

48+
const plural = (n: number) => n === 1 ? '' : 's'
49+
3750
async function resolveNuxtVersion(cwd: string): Promise<string | undefined> {
3851
const nuxtPath = tryResolveNuxt(cwd)
3952
for (const pkg of ['nuxt', 'nuxt-nightly', 'nuxt-edge', 'nuxt3']) {
40-
const pkgJson = await readPackageJSON(pkg, { url: nuxtPath || cwd }).catch(() => null)
41-
if (pkgJson?.version) {
42-
return pkgJson.version
53+
try {
54+
const pkgJson = await readPackageJSON(pkg, { url: nuxtPath || cwd })
55+
if (pkgJson?.version)
56+
return pkgJson.version
57+
}
58+
catch (err: any) {
59+
// Ignore "not found" errors, log unexpected ones
60+
if (err?.code !== 'ERR_MODULE_NOT_FOUND' && err?.code !== 'ENOENT' && !err?.message?.includes('Cannot find'))
61+
log.warn(`Failed to read ${pkg} version: ${err?.message || err}`)
4362
}
4463
}
4564
}
@@ -53,11 +72,20 @@ export default defineCommand({
5372
...cwdArgs,
5473
...legacyRootDirArgs,
5574
...logLevelArgs,
75+
verbose: {
76+
type: 'boolean',
77+
description: 'Show details, suggestions, and URLs',
78+
},
79+
json: {
80+
type: 'boolean',
81+
description: 'Output results as JSON',
82+
},
5683
},
5784
async run(ctx) {
5885
const cwd = resolve(ctx.args.cwd || ctx.args.rootDir)
5986

60-
intro(colors.cyan('Running diagnostics...'))
87+
if (!ctx.args.json)
88+
intro(colors.cyan('Running diagnostics...'))
6189

6290
const { loadNuxt } = await loadKit(cwd)
6391

@@ -72,10 +100,15 @@ export default defineCommand({
72100
})
73101
}
74102
catch (err) {
75-
log.error(colors.red(`Failed to load Nuxt: ${err instanceof Error ? err.message : String(err)}`))
76-
outro(colors.red('Diagnostics failed'))
77-
process.exit(1)
78-
return // unreachable but needed for type narrowing in tests
103+
if (ctx.args.json) {
104+
// eslint-disable-next-line no-console
105+
console.log(JSON.stringify([{ name: 'Nuxt', status: 'error', message: `Failed to load Nuxt: ${err instanceof Error ? err.message : String(err)}` }]))
106+
}
107+
else {
108+
log.error(colors.red(`Failed to load Nuxt: ${err instanceof Error ? err.message : String(err)}`))
109+
outro(colors.red('Diagnostics failed'))
110+
}
111+
return process.exit(1)
79112
}
80113

81114
const checks: DoctorCheck[] = []
@@ -91,7 +124,7 @@ export default defineCommand({
91124
})
92125

93126
// 3. Display results
94-
displayResults(checks)
127+
displayResults(checks, { verbose: ctx.args.verbose, json: ctx.args.json })
95128
}
96129
finally {
97130
await nuxt.close()
@@ -100,28 +133,33 @@ export default defineCommand({
100133
const hasErrors = checks.some(c => c.status === 'error')
101134
const hasWarnings = checks.some(c => c.status === 'warning')
102135

103-
if (hasErrors) {
104-
outro(colors.red('Diagnostics complete with errors'))
105-
process.exit(1)
106-
}
107-
else if (hasWarnings) {
108-
outro(colors.yellow('Diagnostics complete with warnings'))
109-
}
110-
else {
111-
outro(colors.green('All checks passed!'))
136+
if (!ctx.args.json) {
137+
if (hasErrors)
138+
outro(colors.red('Diagnostics complete with errors'))
139+
else if (hasWarnings)
140+
outro(colors.yellow('Diagnostics complete with warnings'))
141+
else
142+
outro(colors.green('All checks passed!'))
112143
}
144+
145+
if (hasErrors)
146+
process.exit(1)
113147
},
114148
})
115149

116150
async function runCoreChecks(checks: DoctorCheck[], nuxt: Nuxt, cwd: string): Promise<void> {
117-
// Version check
118-
await checkVersions(checks, cwd)
119-
120-
// Config validation
121-
checkConfig(checks, nuxt)
151+
const runCheck = async (name: string, fn: () => void | Promise<void>) => {
152+
try {
153+
await fn()
154+
}
155+
catch (err) {
156+
checks.push({ name, status: 'error', message: `Check failed: ${err instanceof Error ? err.message : String(err)}` })
157+
}
158+
}
122159

123-
// Module compatibility
124-
await checkModuleCompat(checks, nuxt, cwd)
160+
await runCheck('Versions', () => checkVersions(checks, cwd))
161+
await runCheck('Config', () => checkConfig(checks, nuxt))
162+
await runCheck('Modules', () => checkModuleCompat(checks, nuxt, cwd))
125163
}
126164

127165
async function checkVersions(checks: DoctorCheck[], cwd: string): Promise<void> {
@@ -142,9 +180,12 @@ async function checkVersions(checks: DoctorCheck[], cwd: string): Promise<void>
142180

143181
if (major < 18) {
144182
checks.push({
183+
id: 'UNSUPPORTED_NODE',
145184
name: 'Versions',
146185
status: 'error',
147186
message: `${runtime}, Nuxt ${nuxtVersion} - Node.js 18+ required`,
187+
suggestion: 'Upgrade Node.js to v18 or later',
188+
url: 'https://nuxt.com/docs/getting-started/installation#prerequisites',
148189
})
149190
return
150191
}
@@ -181,10 +222,13 @@ function checkConfig(checks: DoctorCheck[], nuxt: Nuxt): void {
181222

182223
if (issues.length > 0) {
183224
checks.push({
225+
id: 'CONFIG_ISSUES',
184226
name: 'Config',
185227
status: 'warning',
186-
message: `${issues.length} issue${issues.length > 1 ? 's' : ''} found`,
228+
message: `${issues.length} issue${plural(issues.length)} found`,
187229
details: issues,
230+
suggestion: 'Review nuxt.config.ts and fix the issues above',
231+
url: 'https://nuxt.com/docs/getting-started/configuration',
188232
})
189233
}
190234
else {
@@ -224,17 +268,20 @@ async function checkModuleCompat(checks: DoctorCheck[], nuxt: Nuxt, cwd: string)
224268

225269
if (issues.length > 0) {
226270
checks.push({
271+
id: 'MODULE_COMPAT',
227272
name: 'Modules',
228273
status: 'warning',
229-
message: `${issues.length} incompatible module${issues.length > 1 ? 's' : ''}`,
274+
message: `${issues.length} incompatible module${plural(issues.length)}`,
230275
details: issues,
276+
suggestion: 'Update modules to versions compatible with your Nuxt version',
277+
url: 'https://nuxt.com/modules',
231278
})
232279
}
233280
else if (moduleDetails.length > 0) {
234281
checks.push({
235282
name: 'Modules',
236283
status: 'success',
237-
message: `${moduleDetails.length} module${moduleDetails.length > 1 ? 's' : ''} loaded`,
284+
message: `${moduleDetails.length} module${plural(moduleDetails.length)} loaded`,
238285
details: moduleDetails,
239286
})
240287
}
@@ -247,20 +294,41 @@ async function checkModuleCompat(checks: DoctorCheck[], nuxt: Nuxt, cwd: string)
247294
}
248295
}
249296

250-
function displayResults(checks: DoctorCheck[]): void {
297+
const statusStyles = {
298+
success: { icon: '✓', color: colors.green, detailColor: colors.dim },
299+
warning: { icon: '!', color: colors.yellow, detailColor: colors.yellow },
300+
error: { icon: '✗', color: colors.red, detailColor: colors.red },
301+
} as const
302+
303+
function displayResults(checks: DoctorCheck[], opts: { verbose?: boolean, json?: boolean }): void {
304+
if (opts.json) {
305+
// eslint-disable-next-line no-console
306+
console.log(JSON.stringify(checks))
307+
return
308+
}
309+
251310
for (const check of checks) {
252-
const icon = check.status === 'success' ? colors.green('✓') : check.status === 'warning' ? colors.yellow('!') : colors.red('✗')
311+
const style = statusStyles[check.status]
312+
const icon = style.color(style.icon)
253313
const source = check.source ? colors.gray(` (via ${check.source})`) : ''
254314
const name = colors.bold(check.name)
255-
const message = check.status === 'error' ? colors.red(check.message) : check.status === 'warning' ? colors.yellow(check.message) : check.message
315+
const message = check.status === 'success' ? check.message : style.color(check.message)
256316

257-
// Build output with details on same block
258317
let output = `[${icon}] ${name}${source} - ${message}`
259318

260-
if (check.details?.length) {
261-
const detailColor = check.status === 'error' ? colors.red : check.status === 'warning' ? colors.yellow : colors.dim
262-
for (const detail of check.details) {
263-
output += `\n ${detailColor('→')} ${detailColor(detail)}`
319+
const details = [check.details ?? []].flat()
320+
if (details.length) {
321+
for (const detail of details)
322+
output += `\n ${style.detailColor('→')} ${style.detailColor(detail)}`
323+
}
324+
325+
// Verbose: show suggestion and url
326+
if (opts.verbose) {
327+
if (check.suggestion) {
328+
output += `\n ${colors.cyan('💡')} ${colors.cyan(check.suggestion)}`
329+
}
330+
if (check.url) {
331+
output += `\n ${colors.blue('🔗')} ${colors.blue(check.url)}`
264332
}
265333
}
266334

0 commit comments

Comments
 (0)