Skip to content

Commit cf57ffa

Browse files
authored
feat: discrete npm doctor commands (#5888)
* fix: output doctor checks as they finish * feat: allow for discrete npm doctor checks * feat: add environment check to npm doctor * chore: refactor command
1 parent 83fb125 commit cf57ffa

File tree

5 files changed

+903
-359
lines changed

5 files changed

+903
-359
lines changed

docs/lib/content/commands/npm-doctor.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ Also, in addition to this, there are also very many issue reports due to
3131
using old versions of npm. Since npm is constantly improving, running
3232
`npm@latest` is better than an old version.
3333

34-
`npm doctor` verifies the following items in your environment, and if there
35-
are any recommended changes, it will display them.
34+
`npm doctor` verifies the following items in your environment, and if
35+
there are any recommended changes, it will display them. By default npm
36+
runs all of these checks. You can limit what checks are ran by
37+
specifying them as extra arguments.
3638

3739
#### `npm ping`
3840

lib/commands/doctor.js

+162-68
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
const cacache = require('cacache')
22
const fs = require('fs')
33
const fetch = require('make-fetch-happen')
4-
const table = require('text-table')
4+
const Table = require('cli-table3')
55
const which = require('which')
66
const pacote = require('pacote')
77
const { resolve } = require('path')
88
const semver = require('semver')
99
const { promisify } = require('util')
1010
const log = require('../utils/log-shim.js')
11-
const ansiTrim = require('../utils/ansi-trim.js')
1211
const ping = require('../utils/ping.js')
1312
const {
1413
registry: { default: defaultRegistry },
@@ -17,6 +16,7 @@ const lstat = promisify(fs.lstat)
1716
const readdir = promisify(fs.readdir)
1817
const access = promisify(fs.access)
1918
const { R_OK, W_OK, X_OK } = fs.constants
19+
2020
const maskLabel = mask => {
2121
const label = []
2222
if (mask & R_OK) {
@@ -34,76 +34,105 @@ const maskLabel = mask => {
3434
return label.join(', ')
3535
}
3636

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+
]
37102
const BaseCommand = require('../base-command.js')
38103
class Doctor extends BaseCommand {
39104
static description = 'Check your npm environment'
40105
static name = 'doctor'
41106
static params = ['registry']
42107
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
43116

44117
async exec (args) {
45118
log.info('Running checkup')
119+
let allOk = true
46120

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)
92124

125+
if (!this.npm.silent) {
126+
this.output(['Check', 'Value', 'Recommendation/Notes'].map(h => this.npm.chalk.underline(h)))
127+
}
93128
// 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]
96131
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)
100135
}
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 => {
107136
if (!item[1]) {
108137
allOk = false
109138
item[0] = this.npm.chalk.red(item[0])
@@ -112,18 +141,18 @@ class Doctor extends BaseCommand {
112141
} else {
113142
item[1] = this.npm.chalk.green('ok')
114143
}
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+
}
120147
}
121148

122-
if (!this.npm.silent) {
123-
this.npm.output(table(outTable, tableOpts))
124-
}
125149
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+
}
127156
}
128157
}
129158

@@ -191,6 +220,35 @@ class Doctor extends BaseCommand {
191220
}
192221
}
193222

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+
194252
async checkFilesPermission (root, shouldOwn, mask, missingOk) {
195253
let ok = true
196254

@@ -264,7 +322,7 @@ class Doctor extends BaseCommand {
264322
try {
265323
return await which('git').catch(er => {
266324
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.")
268326
})
269327
} finally {
270328
tracker.finish()
@@ -312,6 +370,42 @@ class Doctor extends BaseCommand {
312370
return `using default registry (${defaultRegistry})`
313371
}
314372
}
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+
}
315409
}
316410

317411
module.exports = Doctor

0 commit comments

Comments
 (0)