|
1 | | -'use strict' |
2 | | - |
3 | | -const ansiTrim = require('./utils/ansi-trim') |
4 | | -const chain = require('slide').chain |
5 | | -const color = require('ansicolors') |
6 | | -const defaultRegistry = require('./config/defaults').defaults.registry |
7 | | -const log = require('npmlog') |
8 | | -const npm = require('./npm') |
9 | | -const output = require('./utils/output') |
10 | | -const path = require('path') |
11 | | -const semver = require('semver') |
12 | | -const styles = require('ansistyles') |
| 1 | +const npm = require('./npm.js') |
| 2 | + |
| 3 | +const chalk = require('chalk') |
| 4 | +const ansiTrim = require('./utils/ansi-trim.js') |
13 | 5 | const table = require('text-table') |
| 6 | +const output = require('./utils/output.js') |
| 7 | +const completion = require('./utils/completion/none.js') |
| 8 | +const usageUtil = require('./utils/usage.js') |
| 9 | +const usage = usageUtil('doctor', 'npm doctor') |
| 10 | +const { resolve } = require('path') |
14 | 11 |
|
15 | | -// steps |
16 | | -const checkFilesPermission = require('./doctor/check-files-permission') |
17 | | -const checkPing = require('./doctor/check-ping') |
18 | | -const getGitPath = require('./doctor/get-git-path') |
19 | | -const getLatestNodejsVersion = require('./doctor/get-latest-nodejs-version') |
20 | | -const getLatestNpmVersion = require('./doctor/get-latest-npm-version') |
21 | | -const verifyCachedFiles = require('./doctor/verify-cached-files') |
| 12 | +const ping = require('./utils/ping.js') |
| 13 | +const checkPing = async () => { |
| 14 | + const tracker = npm.log.newItem('checkPing', 1) |
| 15 | + tracker.info('checkPing', 'Pinging registry') |
| 16 | + try { |
| 17 | + await ping(npm.flatOptions) |
| 18 | + return '' |
| 19 | + } catch (er) { |
| 20 | + if (/^E\d{3}$/.test(er.code || '')) { |
| 21 | + throw er.code.substr(1) + ' ' + er.message |
| 22 | + } else { |
| 23 | + throw er |
| 24 | + } |
| 25 | + } finally { |
| 26 | + tracker.finish() |
| 27 | + } |
| 28 | +} |
22 | 29 |
|
23 | | -const globalNodeModules = path.join(npm.config.globalPrefix, 'lib', 'node_modules') |
24 | | -const localNodeModules = path.join(npm.config.localPrefix, 'node_modules') |
| 30 | +const pacote = require('pacote') |
| 31 | +const getLatestNpmVersion = async () => { |
| 32 | + const tracker = npm.log.newItem('getLatestNpmVersion', 1) |
| 33 | + tracker.info('getLatestNpmVersion', 'Getting npm package information') |
| 34 | + try { |
| 35 | + const latest = (await pacote.manifest('npm@latest', npm.flatOptions)).version |
| 36 | + if (semver.gte(npm.version, latest)) { |
| 37 | + return `current: v${npm.version}, latest: v${latest}` |
| 38 | + } else { |
| 39 | + throw `Use npm v${latest}` |
| 40 | + } |
| 41 | + } finally { |
| 42 | + tracker.finish() |
| 43 | + } |
| 44 | +} |
25 | 45 |
|
26 | | -const usageUtil = require('./utils/usage.js') |
27 | | -const usage = usageUtil('doctor', 'npm doctor') |
28 | | -const completion = require('./utils/completion/none.js') |
| 46 | +const semver = require('semver') |
| 47 | +const fetch = require('make-fetch-happen') |
| 48 | +const getLatestNodejsVersion = async () => { |
| 49 | + // XXX get the latest in the current major as well |
| 50 | + const current = process.version |
| 51 | + const currentRange = `^${current}` |
| 52 | + const url = 'https://nodejs.org/dist/index.json' |
| 53 | + const tracker = npm.log.newItem('getLatestNodejsVersion', 1) |
| 54 | + tracker.info('getLatestNodejsVersion', 'Getting Node.js release information') |
| 55 | + try { |
| 56 | + const res = await fetch(url, { method: 'GET', ...npm.flatOptions }) |
| 57 | + const data = await res.json() |
| 58 | + let maxCurrent = '0.0.0' |
| 59 | + let maxLTS = '0.0.0' |
| 60 | + for (const { lts, version } of data) { |
| 61 | + if (lts && semver.gt(version, maxLTS)) { |
| 62 | + maxLTS = version |
| 63 | + } |
| 64 | + if (semver.satisfies(version, currentRange) && semver.gt(version, maxCurrent)) { |
| 65 | + maxCurrent = version |
| 66 | + } |
| 67 | + } |
| 68 | + const recommended = semver.gt(maxCurrent, maxLTS) ? maxCurrent : maxLTS |
| 69 | + if (semver.gte(process.version, recommended)) { |
| 70 | + return `current: ${current}, recommended: ${recommended}` |
| 71 | + } else { |
| 72 | + throw `Use node ${recommended} (current: ${current})` |
| 73 | + } |
| 74 | + } finally { |
| 75 | + tracker.finish() |
| 76 | + } |
| 77 | +} |
29 | 78 |
|
30 | | -function doctor (args, silent, cb) { |
31 | | - args = args || {} |
32 | | - if (typeof cb !== 'function') { |
33 | | - cb = silent |
34 | | - silent = false |
| 79 | +const { promisify } = require('util') |
| 80 | +const fs = require('fs') |
| 81 | +const { R_OK, W_OK, X_OK } = fs.constants |
| 82 | +const maskLabel = mask => { |
| 83 | + const label = [] |
| 84 | + if (mask & R_OK) { |
| 85 | + label.push('readable') |
| 86 | + } |
| 87 | + if (mask & W_OK) { |
| 88 | + label.push('writable') |
| 89 | + } |
| 90 | + if (mask & X_OK) { |
| 91 | + label.push('executable') |
| 92 | + } |
| 93 | + return label.join(', ') |
| 94 | +} |
| 95 | +const lstat = promisify(fs.lstat) |
| 96 | +const readdir = promisify(fs.readdir) |
| 97 | +const access = promisify(fs.access) |
| 98 | +const isWindows = require('./utils/is-windows.js') |
| 99 | +const checkFilesPermission = async (root, shouldOwn = true, mask = null) => { |
| 100 | + if (mask === null) { |
| 101 | + mask = shouldOwn ? R_OK | W_OK : R_OK |
35 | 102 | } |
36 | 103 |
|
37 | | - const actionsToRun = [ |
38 | | - [checkPing], |
39 | | - [getLatestNpmVersion], |
40 | | - [getLatestNodejsVersion, args['node-url']], |
41 | | - [getGitPath], |
42 | | - [checkFilesPermission, npm.cache, 4, 6], |
43 | | - [checkFilesPermission, globalNodeModules, 4, 4], |
44 | | - [checkFilesPermission, localNodeModules, 6, 6], |
45 | | - [verifyCachedFiles, path.join(npm.cache, '_cacache')] |
46 | | - ] |
| 104 | + let ok = true |
| 105 | + |
| 106 | + const tracker = npm.log.newItem(root, 1) |
| 107 | + |
| 108 | + try { |
| 109 | + const uid = process.getuid() |
| 110 | + const gid = process.getgid() |
| 111 | + const files = new Set([root]) |
| 112 | + for (const f of files) { |
| 113 | + tracker.silly('checkFilesPermission', f.substr(root.length + 1)) |
| 114 | + const st = await lstat(f) |
| 115 | + .catch(er => { |
| 116 | + ok = false |
| 117 | + tracker.warn('checkFilesPermission', 'error getting info for ' + f) |
| 118 | + }) |
| 119 | + |
| 120 | + tracker.completeWork(1) |
47 | 121 |
|
48 | | - log.info('doctor', 'Running checkup') |
49 | | - chain(actionsToRun, function (stderr, stdout) { |
50 | | - if (stderr && stderr.message !== 'not found: git') return cb(stderr) |
51 | | - const list = makePretty(stdout) |
52 | | - let outHead = ['Check', 'Value', 'Recommendation'] |
53 | | - let outBody = list |
54 | | - |
55 | | - if (npm.color) { |
56 | | - outHead = outHead.map(function (item) { |
57 | | - return styles.underline(item) |
58 | | - }) |
59 | | - outBody = outBody.map(function (item) { |
60 | | - if (item[2]) { |
61 | | - item[0] = color.red(item[0]) |
62 | | - item[2] = color.magenta(item[2]) |
| 122 | + if (!st) { |
| 123 | + continue |
| 124 | + } |
| 125 | + |
| 126 | + if (shouldOwn && (uid !== st.uid || gid !== st.gid)) { |
| 127 | + tracker.warn('checkFilesPermission', 'should be owner of ' + f) |
| 128 | + ok = false |
| 129 | + } |
| 130 | + |
| 131 | + if (!st.isDirectory() && !st.isFile()) { |
| 132 | + continue |
| 133 | + } |
| 134 | + |
| 135 | + try { |
| 136 | + await access(f, mask) |
| 137 | + } catch (er) { |
| 138 | + ok = false |
| 139 | + const msg = `Missing permissions on ${f} (expect: ${maskLabel(mask)})` |
| 140 | + tracker.error('checkFilesPermission', msg) |
| 141 | + continue |
| 142 | + } |
| 143 | + |
| 144 | + if (st.isDirectory()) { |
| 145 | + const entries = await readdir(f) |
| 146 | + .catch(er => { |
| 147 | + ok = false |
| 148 | + tracker.warn('checkFilesPermission', 'error reading directory ' + f) |
| 149 | + return [] |
| 150 | + }) |
| 151 | + for (const entry of entries) { |
| 152 | + files.add(resolve(f, entry)) |
63 | 153 | } |
64 | | - return item |
65 | | - }) |
| 154 | + } |
66 | 155 | } |
67 | | - |
68 | | - const outTable = [outHead].concat(outBody) |
69 | | - const tableOpts = { |
70 | | - stringLength: function (s) { return ansiTrim(s).length } |
| 156 | + } finally { |
| 157 | + tracker.finish() |
| 158 | + if (!ok) { |
| 159 | + throw `Check the permissions of files in ${root}` + |
| 160 | + (shouldOwn ? ' (should be owned by current user)' : '') |
| 161 | + } else { |
| 162 | + return '' |
71 | 163 | } |
| 164 | + } |
| 165 | +} |
72 | 166 |
|
73 | | - if (!silent) output(table(outTable, tableOpts)) |
| 167 | +const which = require('which') |
| 168 | +const getGitPath = async () => { |
| 169 | + const tracker = npm.log.newItem('getGitPath', 1) |
| 170 | + tracker.info('getGitPath', 'Finding git in your PATH') |
| 171 | + try { |
| 172 | + return await which('git').catch(er => { |
| 173 | + tracker.warn(er) |
| 174 | + throw "Install git and ensure it's in your PATH." |
| 175 | + }) |
| 176 | + } finally { |
| 177 | + tracker.finish() |
| 178 | + } |
| 179 | +} |
| 180 | + |
| 181 | +const cacache = require('cacache') |
| 182 | +const verifyCachedFiles = async () => { |
| 183 | + const tracker = npm.log.newItem('verifyCachedFiles', 1) |
| 184 | + tracker.info('verifyCachedFiles', 'Verifying the npm cache') |
| 185 | + try { |
| 186 | + const stats = await cacache.verify(npm.flatOptions.cache) |
| 187 | + const { badContentCount, reclaimedCount, missingContent, reclaimedSize } = stats |
| 188 | + if (badContentCount || reclaimedCount || missingContent) { |
| 189 | + if (badContentCount) { |
| 190 | + tracker.warn('verifyCachedFiles', `Corrupted content removed: ${badContentCount}`) |
| 191 | + } |
| 192 | + if (reclaimedCount) { |
| 193 | + tracker.warn('verifyCachedFiles', `Content garbage-collected: ${reclaimedCount} (${reclaimedSize} bytes)`) |
| 194 | + } |
| 195 | + if (missingContent) { |
| 196 | + tracker.warn('verifyCachedFiles', `Missing content: ${missingContent}`) |
| 197 | + } |
| 198 | + tracker.warn('verifyCachedFiles', 'Cache issues have been fixed') |
| 199 | + } |
| 200 | + tracker.info('verifyCachedFiles', `Verification complete. Stats: ${ |
| 201 | + JSON.stringify(stats, null, 2) |
| 202 | + }`) |
| 203 | + return `verified ${stats.verifiedContent} tarballs` |
| 204 | + } finally { |
| 205 | + tracker.finish() |
| 206 | + } |
| 207 | +} |
74 | 208 |
|
75 | | - cb(null, list) |
76 | | - }) |
| 209 | +const { defaults: { registry: defaultRegistry } } = require('./config/defaults.js') |
| 210 | +const checkNpmRegistry = async () => { |
| 211 | + if (npm.flatOptions.registry !== defaultRegistry) { |
| 212 | + throw `Try \`npm config set registry=${defaultRegistry}\`` |
| 213 | + } else { |
| 214 | + return `using default registry (${defaultRegistry})` |
| 215 | + } |
77 | 216 | } |
78 | 217 |
|
79 | | -function makePretty (p) { |
80 | | - const ping = p[1] |
81 | | - const npmLTS = p[2] |
82 | | - const nodeLTS = p[3].replace('v', '') |
83 | | - const whichGit = p[4] || 'not installed' |
84 | | - const readbleCaches = p[5] ? 'ok' : 'notOk' |
85 | | - const executableGlobalModules = p[6] ? 'ok' : 'notOk' |
86 | | - const executableLocalModules = p[7] ? 'ok' : 'notOk' |
87 | | - const cacheStatus = p[8] ? `verified ${p[8].verifiedContent} tarballs` : 'notOk' |
88 | | - const npmV = npm.version |
89 | | - const nodeV = process.version.replace('v', '') |
90 | | - const registry = npm.config.get('registry') || '' |
91 | | - const list = [ |
92 | | - ['npm ping', ping], |
93 | | - ['npm -v', 'v' + npmV], |
94 | | - ['node -v', 'v' + nodeV], |
95 | | - ['npm config get registry', registry], |
96 | | - ['which git', whichGit], |
97 | | - ['Perms check on cached files', readbleCaches], |
98 | | - ['Perms check on global node_modules', executableGlobalModules], |
99 | | - ['Perms check on local node_modules', executableLocalModules], |
100 | | - ['Verify cache contents', cacheStatus] |
| 218 | +const cmd = (args, cb) => doctor(args).then(() => cb()).catch(cb) |
| 219 | + |
| 220 | +const doctor = async args => { |
| 221 | + npm.log.info('Running checkup') |
| 222 | + |
| 223 | + // each message is [title, ok, message] |
| 224 | + const messages = [] |
| 225 | + |
| 226 | + const actions = [ |
| 227 | + ['npm ping', checkPing, []], |
| 228 | + ['npm -v', getLatestNpmVersion, []], |
| 229 | + ['node -v', getLatestNodejsVersion, []], |
| 230 | + ['npm config get registry', checkNpmRegistry, []], |
| 231 | + ['which git', getGitPath, []], |
| 232 | + ...(isWindows ? [] : [ |
| 233 | + ['Perms check on cached files', checkFilesPermission, [npm.cache, true, R_OK]], |
| 234 | + ['Perms check on local node_modules', checkFilesPermission, [npm.localDir, true]], |
| 235 | + ['Perms check on global node_modules', checkFilesPermission, [npm.globalDir, false]], |
| 236 | + ['Perms check on local bin folder', checkFilesPermission, [npm.localBin, false, R_OK | W_OK | X_OK]], |
| 237 | + ['Perms check on global bin folder', checkFilesPermission, [npm.globalBin, false, X_OK]] |
| 238 | + ]), |
| 239 | + ['Verify cache contents', verifyCachedFiles, [npm.flatOptions.cache]] |
| 240 | + // TODO: |
| 241 | + // - ensure arborist.loadActual() runs without errors and no invalid edges |
| 242 | + // - ensure package-lock.json matches loadActual() |
| 243 | + // - verify loadActual without hidden lock file matches hidden lockfile |
| 244 | + // - verify all local packages have bins linked |
101 | 245 | ] |
102 | 246 |
|
103 | | - if (p[0] !== 200) list[0][2] = 'Check your internet connection' |
104 | | - if (!semver.satisfies(npmV, '>=' + npmLTS)) list[1][2] = 'Use npm v' + npmLTS |
105 | | - if (!semver.satisfies(nodeV, '>=' + nodeLTS)) list[2][2] = 'Use node v' + nodeLTS |
106 | | - if (registry !== defaultRegistry) list[3][2] = 'Try `npm config set registry ' + defaultRegistry + '`' |
107 | | - if (whichGit === 'not installed') list[4][2] = 'Install git and ensure it\'s in your PATH.' |
108 | | - if (readbleCaches !== 'ok') list[5][2] = 'Check the permissions of your files in ' + npm.config.get('cache') |
109 | | - if (executableGlobalModules !== 'ok') list[6][2] = globalNodeModules + ' must be readable and writable by the current user.' |
110 | | - if (executableLocalModules !== 'ok') list[7][2] = localNodeModules + ' must be readable and writable by the current user.' |
| 247 | + for (const [msg, fn, args] of actions) { |
| 248 | + const line = [msg] |
| 249 | + try { |
| 250 | + line.push(true, await fn(...args)) |
| 251 | + } catch (er) { |
| 252 | + line.push(false, er) |
| 253 | + } |
| 254 | + messages.push(line) |
| 255 | + } |
| 256 | + |
| 257 | + const silent = npm.log.levels[npm.log.level] > npm.log.levels.error |
111 | 258 |
|
112 | | - return list |
| 259 | + const outHead = ['Check', 'Value', 'Recommendation/Notes'] |
| 260 | + .map(!npm.color ? h => h : h => chalk.underline(h)) |
| 261 | + let allOk = true |
| 262 | + const outBody = messages.map(!npm.color |
| 263 | + ? item => { |
| 264 | + allOk = allOk && item[1] |
| 265 | + item[1] = item[1] ? 'ok' : 'not ok' |
| 266 | + item[2] = String(item[2]) |
| 267 | + return item |
| 268 | + } |
| 269 | + : item => { |
| 270 | + allOk = allOk && item[1] |
| 271 | + if (!item[1]) { |
| 272 | + item[0] = chalk.red(item[0]) |
| 273 | + item[2] = chalk.magenta(String(item[2])) |
| 274 | + } |
| 275 | + item[1] = item[1] ? chalk.green('ok') : chalk.red('not ok') |
| 276 | + return item |
| 277 | + }) |
| 278 | + const outTable = [outHead, ...outBody] |
| 279 | + const tableOpts = { |
| 280 | + stringLength: s => ansiTrim(s).length |
| 281 | + } |
| 282 | + |
| 283 | + if (!silent) { |
| 284 | + output(table(outTable, tableOpts)) |
| 285 | + if (!allOk) { |
| 286 | + console.error('') |
| 287 | + } |
| 288 | + } |
| 289 | + if (!allOk) { |
| 290 | + throw 'Some problems found. See above for recommendations.' |
| 291 | + } |
113 | 292 | } |
114 | 293 |
|
115 | | -module.exports = Object.assign(doctor, { completion, usage }) |
| 294 | +module.exports = Object.assign(cmd, { completion, usage }) |
0 commit comments