Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: discrete npm doctor commands #5888

Merged
merged 4 commits into from Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
181 changes: 118 additions & 63 deletions lib/commands/doctor.js
@@ -1,14 +1,13 @@
const cacache = require('cacache')
const fs = require('fs')
const fetch = require('make-fetch-happen')
const table = require('text-table')
const Table = require('cli-table3')
const which = require('which')
const pacote = require('pacote')
const { resolve } = require('path')
const semver = require('semver')
const { promisify } = require('util')
const log = require('../utils/log-shim.js')
const ansiTrim = require('../utils/ansi-trim.js')
const ping = require('../utils/ping.js')
const {
registry: { default: defaultRegistry },
Expand All @@ -17,6 +16,7 @@ const lstat = promisify(fs.lstat)
const readdir = promisify(fs.readdir)
const access = promisify(fs.access)
const { R_OK, W_OK, X_OK } = fs.constants

const maskLabel = mask => {
const label = []
if (mask & R_OK) {
Expand All @@ -43,67 +43,22 @@ class Doctor extends BaseCommand {

async exec (args) {
log.info('Running checkup')
let allOk = true

// each message is [title, ok, message]
const messages = []

const actions = [
['npm ping', 'checkPing', []],
['npm -v', 'getLatestNpmVersion', []],
['node -v', 'getLatestNodejsVersion', []],
['npm config get registry', 'checkNpmRegistry', []],
['which git', 'getGitPath', []],
...(process.platform === 'win32'
? []
: [
[
'Perms check on cached files',
'checkFilesPermission',
[this.npm.cache, true, R_OK],
], [
'Perms check on local node_modules',
'checkFilesPermission',
[this.npm.localDir, true, R_OK | W_OK, true],
], [
'Perms check on global node_modules',
'checkFilesPermission',
[this.npm.globalDir, false, R_OK],
], [
'Perms check on local bin folder',
'checkFilesPermission',
[this.npm.localBin, false, R_OK | W_OK | X_OK, true],
], [
'Perms check on global bin folder',
'checkFilesPermission',
[this.npm.globalBin, false, X_OK],
],
]),
[
'Verify cache contents',
'verifyCachedFiles',
[this.npm.flatOptions.cache],
],
// TODO:
// - ensure arborist.loadActual() runs without errors and no invalid edges
// - ensure package-lock.json matches loadActual()
// - verify loadActual without hidden lock file matches hidden lockfile
// - verify all local packages have bins linked
]
const actions = this.actions(args)
this.checkWidth = actions.reduce((length, item) => Math.max(item[0].length, length), 5)

if (!this.npm.silent) {
this.output(['Check', 'Value', 'Recommendation/Notes'].map(h => this.npm.chalk.underline(h)))
}
// Do the actual work
for (const [msg, fn, args] of actions) {
const line = [msg]
const item = [msg]
try {
line.push(true, await this[fn](...args))
item.push(true, await this[fn](...args))
} catch (er) {
line.push(false, er)
item.push(false, er)
}
messages.push(line)
}

const outHead = ['Check', 'Value', 'Recommendation/Notes'].map(h => this.npm.chalk.underline(h))
let allOk = true
const outBody = messages.map(item => {
if (!item[1]) {
allOk = false
item[0] = this.npm.chalk.red(item[0])
Expand All @@ -112,15 +67,13 @@ class Doctor extends BaseCommand {
} else {
item[1] = this.npm.chalk.green('ok')
}
return item
})
const outTable = [outHead, ...outBody]
const tableOpts = {
stringLength: s => ansiTrim(s).length,
if (!this.npm.silent) {
this.output(item)
}
}

if (!this.npm.silent) {
this.npm.output(table(outTable, tableOpts))
// this.npm.output(table(outTable, tableOpts))
}
if (!allOk) {
throw new Error('Some problems found. See above for recommendations.')
Expand Down Expand Up @@ -191,6 +144,15 @@ class Doctor extends BaseCommand {
}
}

async checkBinPath (dir) {
const tracker = log.newItem('checkBinPath', 1)
tracker.info('checkBinPath', 'Finding npm global bin in your PATH')
if (!process.env.PATH.includes(this.npm.globalBin)) {
throw new Error(`Add ${this.npm.globalBin} to your $PATH`)
}
return this.npm.globalBin
}

async checkFilesPermission (root, shouldOwn, mask, missingOk) {
let ok = true

Expand Down Expand Up @@ -264,7 +226,7 @@ class Doctor extends BaseCommand {
try {
return await which('git').catch(er => {
tracker.warn(er)
throw "Install git and ensure it's in your PATH."
throw new Error("Install git and ensure it's in your PATH.")
})
} finally {
tracker.finish()
Expand Down Expand Up @@ -312,6 +274,99 @@ class Doctor extends BaseCommand {
return `using default registry (${defaultRegistry})`
}
}

output (row) {
const t = new Table({
chars: { top: '',
'top-mid': '',
'top-left': '',
'top-right': '',
bottom: '',
'bottom-mid': '',
'bottom-left': '',
'bottom-right': '',
left: '',
'left-mid': '',
mid: '',
'mid-mid': '',
right: '',
'right-mid': '',
middle: ' ' },
style: { 'padding-left': 0, 'padding-right': 0 },
colWidths: [this.checkWidth, 6],
})
t.push(row)
this.npm.output(t.toString())
}

actions (subcmds) {
const a = []
if (!subcmds.length || subcmds.includes('ping')) {
a.push(['npm ping', 'checkPing', []])
}
if (!subcmds.length || subcmds.includes('versions')) {
a.push(['npm -v', 'getLatestNpmVersion', []])
a.push(['node -v', 'getLatestNodejsVersion', []])
}
if (!subcmds.length || subcmds.includes('registry')) {
a.push(['npm config get registry', 'checkNpmRegistry', []])
}
if (!subcmds.length || subcmds.includes('git')) {
a.push(['which git', 'getGitPath', []])
}
if (!subcmds.length || subcmds.includes('permissions')) {
if (subcmds.includes('permissions') && process.platform === 'win32') {
log.warn('Ignoring permissions checks for windows')
} else if (process.platform !== 'win32') {
a.push([
'Perms check on cached files',
'checkFilesPermission',
[this.npm.cache, true, R_OK],
])
a.push([
'Perms check on local node_modules',
'checkFilesPermission',
[this.npm.localDir, true, R_OK | W_OK, true],
])
a.push([
'Perms check on global node_modules',
'checkFilesPermission',
[this.npm.globalDir, false, R_OK],
])
a.push([
'Perms check on local bin folder',
'checkFilesPermission',
[this.npm.localBin, false, R_OK | W_OK | X_OK, true],
])
a.push([
'Perms check on global bin folder',
'checkFilesPermission',
[this.npm.globalBin, false, X_OK],
])
}
}
if (!subcmds.length || subcmds.includes('cache')) {
a.push([
'Verify cache contents',
'verifyCachedFiles',
[this.npm.flatOptions.cache],
])
}

if (!subcmds.length || subcmds.includes('environment')) {
a.push(['Global bin folder in PATH', 'checkBinPath', []])
}

// TODO:
// subcmd === 'dependencies'?
// - ensure arborist.loadActual() runs without errors and no invalid edges
// - ensure package-lock.json matches loadActual()
// - verify loadActual without hidden lock file matches hidden lockfile
// subcmd === '???'
// - verify all local packages have bins linked
// What is the fix for these?
return a
}
}

module.exports = Doctor