1
1
const cacache = require ( 'cacache' )
2
2
const fs = require ( 'fs' )
3
3
const fetch = require ( 'make-fetch-happen' )
4
- const table = require ( 'text-table ' )
4
+ const Table = require ( 'cli-table3 ' )
5
5
const which = require ( 'which' )
6
6
const pacote = require ( 'pacote' )
7
7
const { resolve } = require ( 'path' )
8
8
const semver = require ( 'semver' )
9
9
const { promisify } = require ( 'util' )
10
10
const log = require ( '../utils/log-shim.js' )
11
- const ansiTrim = require ( '../utils/ansi-trim.js' )
12
11
const ping = require ( '../utils/ping.js' )
13
12
const {
14
13
registry : { default : defaultRegistry } ,
@@ -17,6 +16,7 @@ const lstat = promisify(fs.lstat)
17
16
const readdir = promisify ( fs . readdir )
18
17
const access = promisify ( fs . access )
19
18
const { R_OK , W_OK , X_OK } = fs . constants
19
+
20
20
const maskLabel = mask => {
21
21
const label = [ ]
22
22
if ( mask & R_OK ) {
@@ -34,76 +34,105 @@ const maskLabel = mask => {
34
34
return label . join ( ', ' )
35
35
}
36
36
37
+ const subcommands = [
38
+ {
39
+ groups : [ 'ping' , 'registry' ] ,
40
+ title : 'npm ping' ,
41
+ cmd : 'checkPing' ,
42
+ } , {
43
+ groups : [ 'versions' ] ,
44
+ title : 'npm -v' ,
45
+ cmd : 'getLatestNpmVersion' ,
46
+ } , {
47
+ groups : [ 'versions' ] ,
48
+ title : 'node -v' ,
49
+ cmd : 'getLatestNodejsVersion' ,
50
+ } , {
51
+ groups : [ 'registry' ] ,
52
+ title : 'npm config get registry' ,
53
+ cmd : 'checkNpmRegistry' ,
54
+ } , {
55
+ groups : [ 'environment' ] ,
56
+ title : 'git executable in PATH' ,
57
+ cmd : 'getGitPath' ,
58
+ } , {
59
+ groups : [ 'environment' ] ,
60
+ title : 'global bin folder in PATH' ,
61
+ cmd : 'getBinPath' ,
62
+ } , {
63
+ groups : [ 'permissions' , 'cache' ] ,
64
+ title : 'Perms check on cached files' ,
65
+ cmd : 'checkCachePermission' ,
66
+ windows : false ,
67
+ } , {
68
+ groups : [ 'permissions' ] ,
69
+ title : 'Perms check on local node_modules' ,
70
+ cmd : 'checkLocalModulesPermission' ,
71
+ windows : false ,
72
+ } , {
73
+ groups : [ 'permissions' ] ,
74
+ title : 'Perms check on global node_modules' ,
75
+ cmd : 'checkGlobalModulesPermission' ,
76
+ windows : false ,
77
+ } , {
78
+ groups : [ 'permissions' ] ,
79
+ title : 'Perms check on local bin folder' ,
80
+ cmd : 'checkLocalBinPermission' ,
81
+ windows : false ,
82
+ } , {
83
+ groups : [ 'permissions' ] ,
84
+ title : 'Perms check on global bin folder' ,
85
+ cmd : 'checkGlobalBinPermission' ,
86
+ windows : false ,
87
+ } , {
88
+ groups : [ 'cache' ] ,
89
+ title : 'Verify cache contents' ,
90
+ cmd : 'verifyCachedFiles' ,
91
+ windows : false ,
92
+ } ,
93
+ // TODO:
94
+ // group === 'dependencies'?
95
+ // - ensure arborist.loadActual() runs without errors and no invalid edges
96
+ // - ensure package-lock.json matches loadActual()
97
+ // - verify loadActual without hidden lock file matches hidden lockfile
98
+ // group === '???'
99
+ // - verify all local packages have bins linked
100
+ // What is the fix for these?
101
+ ]
37
102
const BaseCommand = require ( '../base-command.js' )
38
103
class Doctor extends BaseCommand {
39
104
static description = 'Check your npm environment'
40
105
static name = 'doctor'
41
106
static params = [ 'registry' ]
42
107
static ignoreImplicitWorkspace = false
108
+ static usage = [ `[${ subcommands . flatMap ( s => s . groups )
109
+ . filter ( ( value , index , self ) => self . indexOf ( value ) === index )
110
+ . join ( '] [' ) } ]`]
111
+
112
+ static subcommands = subcommands
113
+
114
+ // minimum width of check column, enough for the word `Check`
115
+ #checkWidth = 5
43
116
44
117
async exec ( args ) {
45
118
log . info ( 'Running checkup' )
119
+ let allOk = true
46
120
47
- // each message is [title, ok, message]
48
- const messages = [ ]
49
-
50
- const actions = [
51
- [ 'npm ping' , 'checkPing' , [ ] ] ,
52
- [ 'npm -v' , 'getLatestNpmVersion' , [ ] ] ,
53
- [ 'node -v' , 'getLatestNodejsVersion' , [ ] ] ,
54
- [ 'npm config get registry' , 'checkNpmRegistry' , [ ] ] ,
55
- [ 'which git' , 'getGitPath' , [ ] ] ,
56
- ...( process . platform === 'win32'
57
- ? [ ]
58
- : [
59
- [
60
- 'Perms check on cached files' ,
61
- 'checkFilesPermission' ,
62
- [ this . npm . cache , true , R_OK ] ,
63
- ] , [
64
- 'Perms check on local node_modules' ,
65
- 'checkFilesPermission' ,
66
- [ this . npm . localDir , true , R_OK | W_OK , true ] ,
67
- ] , [
68
- 'Perms check on global node_modules' ,
69
- 'checkFilesPermission' ,
70
- [ this . npm . globalDir , false , R_OK ] ,
71
- ] , [
72
- 'Perms check on local bin folder' ,
73
- 'checkFilesPermission' ,
74
- [ this . npm . localBin , false , R_OK | W_OK | X_OK , true ] ,
75
- ] , [
76
- 'Perms check on global bin folder' ,
77
- 'checkFilesPermission' ,
78
- [ this . npm . globalBin , false , X_OK ] ,
79
- ] ,
80
- ] ) ,
81
- [
82
- 'Verify cache contents' ,
83
- 'verifyCachedFiles' ,
84
- [ this . npm . flatOptions . cache ] ,
85
- ] ,
86
- // TODO:
87
- // - ensure arborist.loadActual() runs without errors and no invalid edges
88
- // - ensure package-lock.json matches loadActual()
89
- // - verify loadActual without hidden lock file matches hidden lockfile
90
- // - verify all local packages have bins linked
91
- ]
121
+ const actions = this . actions ( args )
122
+ this . #checkWidth = actions . reduce ( ( length , item ) =>
123
+ Math . max ( item . title . length , length ) , this . #checkWidth)
92
124
125
+ if ( ! this . npm . silent ) {
126
+ this . output ( [ 'Check' , 'Value' , 'Recommendation/Notes' ] . map ( h => this . npm . chalk . underline ( h ) ) )
127
+ }
93
128
// Do the actual work
94
- for ( const [ msg , fn , args ] of actions ) {
95
- const line = [ msg ]
129
+ for ( const { title , cmd } of actions ) {
130
+ const item = [ title ]
96
131
try {
97
- line . push ( true , await this [ fn ] ( ... args ) )
98
- } catch ( er ) {
99
- line . push ( false , er )
132
+ item . push ( true , await this [ cmd ] ( ) )
133
+ } catch ( err ) {
134
+ item . push ( false , err )
100
135
}
101
- messages . push ( line )
102
- }
103
-
104
- const outHead = [ 'Check' , 'Value' , 'Recommendation/Notes' ] . map ( h => this . npm . chalk . underline ( h ) )
105
- let allOk = true
106
- const outBody = messages . map ( item => {
107
136
if ( ! item [ 1 ] ) {
108
137
allOk = false
109
138
item [ 0 ] = this . npm . chalk . red ( item [ 0 ] )
@@ -112,18 +141,18 @@ class Doctor extends BaseCommand {
112
141
} else {
113
142
item [ 1 ] = this . npm . chalk . green ( 'ok' )
114
143
}
115
- return item
116
- } )
117
- const outTable = [ outHead , ...outBody ]
118
- const tableOpts = {
119
- stringLength : s => ansiTrim ( s ) . length ,
144
+ if ( ! this . npm . silent ) {
145
+ this . output ( item )
146
+ }
120
147
}
121
148
122
- if ( ! this . npm . silent ) {
123
- this . npm . output ( table ( outTable , tableOpts ) )
124
- }
125
149
if ( ! allOk ) {
126
- throw new Error ( 'Some problems found. See above for recommendations.' )
150
+ if ( this . npm . silent ) {
151
+ /* eslint-disable-next-line max-len */
152
+ throw new Error ( 'Some problems found. Check logs or disable silent mode for recommendations.' )
153
+ } else {
154
+ throw new Error ( 'Some problems found. See above for recommendations.' )
155
+ }
127
156
}
128
157
}
129
158
@@ -191,6 +220,35 @@ class Doctor extends BaseCommand {
191
220
}
192
221
}
193
222
223
+ async getBinPath ( dir ) {
224
+ const tracker = log . newItem ( 'getBinPath' , 1 )
225
+ tracker . info ( 'getBinPath' , 'Finding npm global bin in your PATH' )
226
+ if ( ! process . env . PATH . includes ( this . npm . globalBin ) ) {
227
+ throw new Error ( `Add ${ this . npm . globalBin } to your $PATH` )
228
+ }
229
+ return this . npm . globalBin
230
+ }
231
+
232
+ async checkCachePermission ( ) {
233
+ return this . checkFilesPermission ( this . npm . cache , true , R_OK )
234
+ }
235
+
236
+ async checkLocalModulesPermission ( ) {
237
+ return this . checkFilesPermission ( this . npm . localDir , true , R_OK | W_OK , true )
238
+ }
239
+
240
+ async checkGlobalModulesPermission ( ) {
241
+ return this . checkFilesPermission ( this . npm . globalDir , false , R_OK )
242
+ }
243
+
244
+ async checkLocalBinPermission ( ) {
245
+ return this . checkFilesPermission ( this . npm . localBin , false , R_OK | W_OK | X_OK , true )
246
+ }
247
+
248
+ async checkGlobalBinPermission ( ) {
249
+ return this . checkFilesPermission ( this . npm . globalBin , false , X_OK )
250
+ }
251
+
194
252
async checkFilesPermission ( root , shouldOwn , mask , missingOk ) {
195
253
let ok = true
196
254
@@ -264,7 +322,7 @@ class Doctor extends BaseCommand {
264
322
try {
265
323
return await which ( 'git' ) . catch ( er => {
266
324
tracker . warn ( er )
267
- throw "Install git and ensure it's in your PATH."
325
+ throw new Error ( "Install git and ensure it's in your PATH." )
268
326
} )
269
327
} finally {
270
328
tracker . finish ( )
@@ -312,6 +370,42 @@ class Doctor extends BaseCommand {
312
370
return `using default registry (${ defaultRegistry } )`
313
371
}
314
372
}
373
+
374
+ output ( row ) {
375
+ const t = new Table ( {
376
+ chars : { top : '' ,
377
+ 'top-mid' : '' ,
378
+ 'top-left' : '' ,
379
+ 'top-right' : '' ,
380
+ bottom : '' ,
381
+ 'bottom-mid' : '' ,
382
+ 'bottom-left' : '' ,
383
+ 'bottom-right' : '' ,
384
+ left : '' ,
385
+ 'left-mid' : '' ,
386
+ mid : '' ,
387
+ 'mid-mid' : '' ,
388
+ right : '' ,
389
+ 'right-mid' : '' ,
390
+ middle : ' ' } ,
391
+ style : { 'padding-left' : 0 , 'padding-right' : 0 } ,
392
+ colWidths : [ this . #checkWidth, 6 ] ,
393
+ } )
394
+ t . push ( row )
395
+ this . npm . output ( t . toString ( ) )
396
+ }
397
+
398
+ actions ( params ) {
399
+ return this . constructor . subcommands . filter ( subcmd => {
400
+ if ( process . platform === 'win32' && subcmd . windows === false ) {
401
+ return false
402
+ }
403
+ if ( params . length ) {
404
+ return params . some ( param => subcmd . groups . includes ( param ) )
405
+ }
406
+ return true
407
+ } )
408
+ }
315
409
}
316
410
317
411
module . exports = Doctor
0 commit comments