@@ -13,33 +13,50 @@ import { isBun, isDeno } from 'std-env'
1313import { loadKit , tryResolveNuxt } from '../utils/kit'
1414import { cwdArgs , legacyRootDirArgs , logLevelArgs } from './_shared'
1515
16- // Types for the hook-based architecture (will move to @nuxt/schema)
1716interface DoctorCheck {
17+ // Required
1818 name : string
1919 status : 'success' | 'warning' | 'error'
2020 message : string
21- source ?: string // module name, e.g. "@nuxt/a11y"
22- details ?: string [ ] // list of issues/details to display
21+
22+ // Optional - identity/origin
23+ id ?: string // programmatic code: "MISSING_PEER_DEP"
24+ source ?: string // module name: "@nuxt/ui"
25+
26+ // Optional - verbose fields
27+ details ?: string | string [ ]
28+ suggestion ?: string
29+ url ?: string
30+
31+ // Optional - programmatic
32+ data ?: Record < string , unknown >
2333}
2434
2535interface DoctorCheckContext {
2636 addCheck : ( check : DoctorCheck ) => void
2737 nuxt : Nuxt
2838}
2939
30- // Augment NuxtHooks for the doctor:check hook (will be moved to @nuxt/schema)
3140declare module '@nuxt/schema' {
3241 interface NuxtHooks {
3342 'doctor:check' : ( ctx : DoctorCheckContext ) => void | Promise < void >
3443 }
3544}
3645
46+ const plural = ( n : number ) => n === 1 ? '' : 's'
47+
3748async function resolveNuxtVersion ( cwd : string ) : Promise < string | undefined > {
3849 const nuxtPath = tryResolveNuxt ( cwd )
3950 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
51+ try {
52+ const pkgJson = await readPackageJSON ( pkg , { url : nuxtPath || cwd } )
53+ if ( pkgJson ?. version )
54+ return pkgJson . version
55+ }
56+ catch ( err : any ) {
57+ // Ignore "not found" errors, log unexpected ones
58+ if ( err ?. code !== 'ERR_MODULE_NOT_FOUND' && err ?. code !== 'ENOENT' && ! err ?. message ?. includes ( 'Cannot find' ) )
59+ log . warn ( `Failed to read ${ pkg } version: ${ err ?. message || err } ` )
4360 }
4461 }
4562}
@@ -53,11 +70,20 @@ export default defineCommand({
5370 ...cwdArgs ,
5471 ...legacyRootDirArgs ,
5572 ...logLevelArgs ,
73+ verbose : {
74+ type : 'boolean' ,
75+ description : 'Show details, suggestions, and URLs' ,
76+ } ,
77+ json : {
78+ type : 'boolean' ,
79+ description : 'Output results as JSON' ,
80+ } ,
5681 } ,
5782 async run ( ctx ) {
5883 const cwd = resolve ( ctx . args . cwd || ctx . args . rootDir )
5984
60- intro ( colors . cyan ( 'Running diagnostics...' ) )
85+ if ( ! ctx . args . json )
86+ intro ( colors . cyan ( 'Running diagnostics...' ) )
6187
6288 const { loadNuxt } = await loadKit ( cwd )
6389
@@ -72,10 +98,15 @@ export default defineCommand({
7298 } )
7399 }
74100 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
101+ if ( ctx . args . json ) {
102+ // eslint-disable-next-line no-console
103+ console . log ( JSON . stringify ( [ { name : 'Nuxt' , status : 'error' , message : `Failed to load Nuxt: ${ err instanceof Error ? err . message : String ( err ) } ` } ] ) )
104+ }
105+ else {
106+ log . error ( colors . red ( `Failed to load Nuxt: ${ err instanceof Error ? err . message : String ( err ) } ` ) )
107+ outro ( colors . red ( 'Diagnostics failed' ) )
108+ }
109+ return process . exit ( 1 )
79110 }
80111
81112 const checks : DoctorCheck [ ] = [ ]
@@ -91,7 +122,7 @@ export default defineCommand({
91122 } )
92123
93124 // 3. Display results
94- displayResults ( checks )
125+ displayResults ( checks , { verbose : ctx . args . verbose , json : ctx . args . json } )
95126 }
96127 finally {
97128 await nuxt . close ( )
@@ -100,28 +131,33 @@ export default defineCommand({
100131 const hasErrors = checks . some ( c => c . status === 'error' )
101132 const hasWarnings = checks . some ( c => c . status === 'warning' )
102133
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!' ) )
134+ if ( ! ctx . args . json ) {
135+ if ( hasErrors )
136+ outro ( colors . red ( 'Diagnostics complete with errors' ) )
137+ else if ( hasWarnings )
138+ outro ( colors . yellow ( 'Diagnostics complete with warnings' ) )
139+ else
140+ outro ( colors . green ( 'All checks passed!' ) )
112141 }
142+
143+ if ( hasErrors )
144+ process . exit ( 1 )
113145 } ,
114146} )
115147
116148async 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 )
149+ const runCheck = async ( name : string , fn : ( ) => void | Promise < void > ) => {
150+ try {
151+ await fn ( )
152+ }
153+ catch ( err ) {
154+ checks . push ( { name, status : 'error' , message : `Check failed: ${ err instanceof Error ? err . message : String ( err ) } ` } )
155+ }
156+ }
122157
123- // Module compatibility
124- await checkModuleCompat ( checks , nuxt , cwd )
158+ await runCheck ( 'Versions' , ( ) => checkVersions ( checks , cwd ) )
159+ await runCheck ( 'Config' , ( ) => checkConfig ( checks , nuxt ) )
160+ await runCheck ( 'Modules' , ( ) => checkModuleCompat ( checks , nuxt , cwd ) )
125161}
126162
127163async function checkVersions ( checks : DoctorCheck [ ] , cwd : string ) : Promise < void > {
@@ -137,14 +173,14 @@ async function checkVersions(checks: DoctorCheck[], cwd: string): Promise<void>
137173
138174 // Check Node.js version (if not Bun/Deno)
139175 if ( ! isBun && ! isDeno ) {
140- const nodeVersion = process . version
141- const major = Number . parseInt ( nodeVersion . slice ( 1 ) . split ( '.' ) [ 0 ] || '0' , 10 )
142-
143- if ( major < 18 ) {
176+ if ( ! semverSatisfies ( process . versions . node , '>= 18.0.0' ) ) {
144177 checks . push ( {
178+ id : 'UNSUPPORTED_NODE' ,
145179 name : 'Versions' ,
146180 status : 'error' ,
147181 message : `${ runtime } , Nuxt ${ nuxtVersion } - Node.js 18+ required` ,
182+ suggestion : 'Upgrade Node.js to v18 or later' ,
183+ url : 'https://nuxt.com/docs/getting-started/installation#prerequisites' ,
148184 } )
149185 return
150186 }
@@ -181,10 +217,13 @@ function checkConfig(checks: DoctorCheck[], nuxt: Nuxt): void {
181217
182218 if ( issues . length > 0 ) {
183219 checks . push ( {
220+ id : 'CONFIG_ISSUES' ,
184221 name : 'Config' ,
185222 status : 'warning' ,
186- message : `${ issues . length } issue${ issues . length > 1 ? 's' : '' } found` ,
223+ message : `${ issues . length } issue${ plural ( issues . length ) } found` ,
187224 details : issues ,
225+ suggestion : 'Review nuxt.config.ts and fix the issues above' ,
226+ url : 'https://nuxt.com/docs/getting-started/configuration' ,
188227 } )
189228 }
190229 else {
@@ -224,17 +263,20 @@ async function checkModuleCompat(checks: DoctorCheck[], nuxt: Nuxt, cwd: string)
224263
225264 if ( issues . length > 0 ) {
226265 checks . push ( {
266+ id : 'MODULE_COMPAT' ,
227267 name : 'Modules' ,
228268 status : 'warning' ,
229- message : `${ issues . length } incompatible module${ issues . length > 1 ? 's' : '' } ` ,
269+ message : `${ issues . length } incompatible module${ plural ( issues . length ) } ` ,
230270 details : issues ,
271+ suggestion : 'Update modules to versions compatible with your Nuxt version' ,
272+ url : 'https://nuxt.com/modules' ,
231273 } )
232274 }
233275 else if ( moduleDetails . length > 0 ) {
234276 checks . push ( {
235277 name : 'Modules' ,
236278 status : 'success' ,
237- message : `${ moduleDetails . length } module${ moduleDetails . length > 1 ? 's' : '' } loaded` ,
279+ message : `${ moduleDetails . length } module${ plural ( moduleDetails . length ) } loaded` ,
238280 details : moduleDetails ,
239281 } )
240282 }
@@ -247,20 +289,41 @@ async function checkModuleCompat(checks: DoctorCheck[], nuxt: Nuxt, cwd: string)
247289 }
248290}
249291
250- function displayResults ( checks : DoctorCheck [ ] ) : void {
292+ const statusStyles = {
293+ success : { icon : '✓' , color : colors . green , detailColor : colors . dim } ,
294+ warning : { icon : '!' , color : colors . yellow , detailColor : colors . yellow } ,
295+ error : { icon : '✗' , color : colors . red , detailColor : colors . red } ,
296+ } as const
297+
298+ function displayResults ( checks : DoctorCheck [ ] , opts : { verbose ?: boolean , json ?: boolean } ) : void {
299+ if ( opts . json ) {
300+ // eslint-disable-next-line no-console
301+ console . log ( JSON . stringify ( checks ) )
302+ return
303+ }
304+
251305 for ( const check of checks ) {
252- const icon = check . status === 'success' ? colors . green ( '✓' ) : check . status === 'warning' ? colors . yellow ( '!' ) : colors . red ( '✗' )
306+ const style = statusStyles [ check . status ]
307+ const icon = style . color ( style . icon )
253308 const source = check . source ? colors . gray ( ` (via ${ check . source } )` ) : ''
254309 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
310+ const message = check . status === 'success ' ? check . message : style . color ( check . message )
256311
257- // Build output with details on same block
258312 let output = `[${ icon } ] ${ name } ${ source } - ${ message } `
259313
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 ) } `
314+ const details = [ check . details ?? [ ] ] . flat ( )
315+ if ( details . length ) {
316+ for ( const detail of details )
317+ output += `\n ${ style . detailColor ( '→' ) } ${ style . detailColor ( detail ) } `
318+ }
319+
320+ // Verbose: show suggestion and url
321+ if ( opts . verbose ) {
322+ if ( check . suggestion ) {
323+ output += `\n ${ colors . cyan ( '💡' ) } ${ colors . cyan ( check . suggestion ) } `
324+ }
325+ if ( check . url ) {
326+ output += `\n ${ colors . blue ( '🔗' ) } ${ colors . blue ( check . url ) } `
264327 }
265328 }
266329
0 commit comments