@@ -15,11 +15,22 @@ import { cwdArgs, legacyRootDirArgs, logLevelArgs } from './_shared'
1515
1616// Types for the hook-based architecture (will move to @nuxt/schema)
1717interface 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
2536interface DoctorCheckContext {
@@ -34,12 +45,20 @@ declare module '@nuxt/schema' {
3445 }
3546}
3647
48+ const plural = ( n : number ) => n === 1 ? '' : 's'
49+
3750async 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
116150async 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
127165async 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