codekingpro/portable-devtools
115k
1const { format } = require('node:util')
2const { resolve } = require('node:path')
3const { redactLog: replaceInfo } = require('@npmcli/redact')
4const { log } = require('proc-log')
5
6const errorMessage = (er, npm) => {
7 const summary = []
8 const detail = []
9 const files = []
10 let json
11
12 er.message &&= replaceInfo(er.message)
13 er.stack &&= replaceInfo(er.stack)
14
15 switch (er.code) {
16 case 'ERESOLVE': {
17 const { report } = require('./explain-eresolve.js')
18 summary.push(['ERESOLVE', er.message])
19 detail.push(['', ''])
20 // XXX(display): error messages are logged so we use the logColor since that is based on stderr.
21 // This should be handled solely by the display layer so it could also be printed to stdout if necessary.
22 const { explanation, file } = report(er, npm.logChalk, npm.noColorChalk)
23 detail.push(['', explanation])
24 files.push(['eresolve-report.txt', file])
25 break
26 }
27
28 case 'ENOLOCK': {
29 const cmd = npm.command || ''
30 summary.push([cmd, 'This command requires an existing lockfile.'])
31 detail.push([cmd, 'Try creating one first with: npm i --package-lock-only'])
32 detail.push([cmd, `Original error: ${er.message}`])
33 break
34 }
35
36 case 'ENOAUDIT':
37 summary.push(['audit', er.message])
38 break
39
40 case 'ECONNREFUSED':
41 summary.push(['', er])
42 detail.push(['', [
43 '',
44 `If you are behind a proxy, please make sure that the 'proxy' config is set properly. See: 'npm help config'`,
45 ].join('\n')])
46 break
47
48 case 'EACCES':
49 case 'EPERM': {
50 const isCachePath =
51 typeof er.path === 'string' && npm.loaded && er.path.startsWith(npm.config.get('cache'))
52 const isCacheDest =
53 typeof er.dest === 'string' && npm.loaded && er.dest.startsWith(npm.config.get('cache'))
54
55 if (process.platform !== 'win32' && (isCachePath || isCacheDest)) {
56 // user probably doesn't need this, but still add it to the debug log
57 log.verbose(er.stack)
58 summary.push(['', [
59 '',
60 'Your cache folder contains root-owned files, due to a bug in previous versions of npm which has since been addressed.',
61 '',
62 'To permanently fix this problem, please run:',
63 ` sudo chown -R ${process.getuid()}:${process.getgid()} "${npm.config.get('cache')}"`,
64 ].join('\n')])
65 } else {
66 summary.push(['', er])
67 detail.push(['', [
68 '',
69 'The operation was rejected by your operating system.',
70 ...process.platform === 'win32' ? [
71 `It's possible that the file was already in use (by a text editor or antivirus), or that you lack permissions to access it.`,
72 ] : [
73 'It is likely you do not have the permissions to access this file as the current user',
74 ],
75 '',
76 'If you believe this might be a permissions issue, please double-check the permissions of the file and its containing directories, or try running the command again as root/Administrator.',
77 ].join('\n')])
78 }
79 break
80 }
81
82 case 'EALLOWGIT':
83 summary.push(['', er.message])
84 detail.push(['', `Refusing to fetch "${er.package}"`])
85 break
86
87 case 'ENOGIT':
88 summary.push(['', er.message])
89 detail.push(['', [
90 '',
91 'Failed using git.',
92 'Please check if you have git installed and in your PATH.',
93 ].join('\n')])
94 break
95
96 case 'EJSONPARSE':
97 // Check whether we ran into a conflict in our own package.json
98 if (er.path === resolve(npm.prefix, 'package.json')) {
99 const { isDiff } = require('parse-conflict-json')
100 const txt = require('node:fs').readFileSync(er.path, 'utf8').replace(/\r\n/g, '\n')
101 if (isDiff(txt)) {
102 detail.push(['', [
103 'Merge conflict detected in your package.json.',
104 '',
105 'Please resolve the package.json conflict and retry.',
106 ].join('\n')])
107 break
108 }
109 }
110 summary.push(['JSON.parse', er.message])
111 detail.push(['JSON.parse', [
112 'Failed to parse JSON data.',
113 'Note: package.json must be actual JSON, not just JavaScript.',
114 ].join('\n')])
115 break
116
117 case 'EOTP':
118 case 'E401':
119 // E401 is for places where we accidentally neglect OTP stuff
120 if (er.code === 'EOTP' || /one-time pass/.test(er.message)) {
121 const authUrl = er.body?.authUrl
122 const doneUrl = er.body?.doneUrl
123 if (authUrl && doneUrl) {
124 json = { authUrl, doneUrl }
125 summary.push(['', 'This operation requires a one-time password.'])
126 detail.push(['', `Open this URL in your browser to authenticate:`])
127 detail.push(['', ` ${authUrl}`])
128 detail.push(['', ''])
129 detail.push(['', `After authenticating, your token can be retrieved from:`])
130 detail.push(['', ` ${doneUrl}`])
131 } else {
132 summary.push(['', 'This operation requires a one-time password from your authenticator.'])
133 detail.push(['', [
134 'You can provide a one-time password by passing --otp=<code> to the command you ran.',
135 'If you already provided a one-time password then it is likely that you either typoed it, or it timed out. Please try again.',
136 ].join('\n')])
137 }
138 } else {
139 // npm ERR! code E401
140 // npm ERR! Unable to authenticate, need: Basic
141 const auth = !er.headers || !er.headers['www-authenticate']
142 ? []
143 : er.headers['www-authenticate'].map(au => au.split(/[,\s]+/))[0]
144
145 if (auth.includes('Bearer')) {
146 summary.push(['',
147 'Unable to authenticate, your authentication token seems to be invalid.',
148 ])
149 detail.push(['', [
150 'To correct this please try logging in again with:',
151 ' npm login',
152 ].join('\n')])
153 } else if (auth.includes('Basic')) {
154 summary.push(['', 'Incorrect or missing password.'])
155 detail.push(['', [
156 'If you were trying to login, change your password, create an authentication token or enable two-factor authentication then that means you likely typed your password in incorrectly.',
157 'Please try again, or recover your password at:',
158 ' https://www.npmjs.com/forgot',
159 '',
160 'If you were doing some other operation then your saved credentials are probably out of date.',
161 'To correct this please try logging in again with:',
162 ' npm login',
163 ].join('\n')])
164 } else {
165 summary.push(['', er.message || er])
166 }
167 }
168 break
169
170 case 'E404':
171 // There's no need to have 404 in the message as well.
172 summary.push(['404', er.message.replace(/^404\s+/, '')])
173 if (er.pkgid && er.pkgid !== '-') {
174 const pkg = er.pkgid.replace(/(?!^)@.*$/, '')
175
176 detail.push(['404', ''])
177 detail.push([
178 '404',
179 '',
180 `The requested resource '${replaceInfo(er.pkgid)}' could not be found or you do not have permission to access it.`,
181 ])
182
183 const nameValidator = require('validate-npm-package-name')
184 const valResult = nameValidator(pkg)
185
186 if (!valResult.validForNewPackages) {
187 detail.push(['404', 'This package name is not valid, because', ''])
188
189 const errorsArray = [...(valResult.errors || []), ...(valResult.warnings || [])]
190 errorsArray.forEach((item, idx) => detail.push(['404', ' ' + (idx + 1) + '. ' + item]))
191 }
192
193 detail.push(['404', ''])
194 detail.push(['404', 'Note that you can also install from a'])
195 detail.push(['404', 'tarball, folder, http url, or git url.'])
196 }
197 break
198
199 case 'EPUBLISHCONFLICT':
200 summary.push(['publish fail', 'Cannot publish over existing version.'])
201 detail.push(['publish fail', `Update the 'version' field in package.json and try again.`])
202 detail.push(['publish fail', ''])
203 detail.push(['publish fail', 'To automatically increment version numbers, see:'])
204 detail.push(['publish fail', ' npm help version'])
205 break
206
207 case 'EISGIT':
208 summary.push(['git', er.message])
209 summary.push(['git', ` ${er.path}`])
210 detail.push(['git', [
211 'Refusing to remove it. Update manually, or move it out of the way first.',
212 ].join('\n')])
213 break
214
215 case 'EBADDEVENGINES': {
216 const { current, required } = er
217 summary.push(['EBADDEVENGINES', er.message])
218 detail.push(['EBADDEVENGINES', { current, required }])
219 break
220 }
221
222 case 'EBADPLATFORM': {
223 const actual = er.current
224 const expected = { ...er.required }
225 const checkedKeys = []
226 for (const key in expected) {
227 if (Array.isArray(expected[key]) && expected[key].length > 0) {
228 expected[key] = expected[key].join(',')
229 checkedKeys.push(key)
230 } else if (expected[key] === undefined ||
231 Array.isArray(expected[key]) && expected[key].length === 0) {
232 delete expected[key]
233 delete actual[key]
234 } else {
235 checkedKeys.push(key)
236 }
237 }
238
239 const longestKey = Math.max(...checkedKeys.map((key) => key.length))
240 const detailEntry = []
241 for (const key of checkedKeys) {
242 const padding = key.length === longestKey
243 ? 1
244 : 1 + (longestKey - key.length)
245
246 // padding + 1 because 'actual' is longer than 'valid'
247 detailEntry.push(`Valid ${key}:${' '.repeat(padding + 1)}${expected[key]}`)
248 detailEntry.push(`Actual ${key}:${' '.repeat(padding)}${actual[key]}`)
249 }
250
251 summary.push(['notsup', format(
252 'Unsupported platform for %s: wanted %j (current: %j)',
253 er.pkgid,
254 expected,
255 actual
256 )])
257 detail.push(['notsup', detailEntry.join('\n')])
258 break
259 }
260
261 case 'EEXIST':
262 summary.push(['', er.message])
263 summary.push(['', 'File exists: ' + (er.dest || er.path)])
264 detail.push(['', 'Remove the existing file and try again, or run npm'])
265 detail.push(['', 'with --force to overwrite files recklessly.'])
266 break
267
268 case 'ENEEDAUTH':
269 summary.push(['need auth', er.message])
270 detail.push(['need auth', 'You need to authorize this machine using `npm adduser`'])
271 break
272
273 case 'ECONNRESET':
274 case 'ENOTFOUND':
275 case 'ETIMEDOUT':
276 case 'ERR_SOCKET_TIMEOUT':
277 case 'EAI_FAIL':
278 summary.push(['network', er.message])
279 detail.push(['network', [
280 'This is a problem related to network connectivity.',
281 'In most cases you are behind a proxy or have bad network settings.',
282 '',
283 `If you are behind a proxy, please make sure that the 'proxy' config is set properly. See: 'npm help config'`,
284 ].join('\n')])
285 break
286
287 case 'ETARGET':
288 summary.push(['notarget', er.message])
289 detail.push(['notarget', [
290 `In most cases you or one of your dependencies are requesting a package version that doesn't exist.`,
291 ].join('\n')])
292 break
293
294 case 'E403':
295 summary.push(['403', er.message])
296 detail.push(['403', [
297 'In most cases, you or one of your dependencies are requesting a package version that is forbidden by your security policy, or on a server you do not have access to.',
298 ].join('\n')])
299 break
300
301 case 'EBADENGINE':
302 summary.push(['engine', er.message])
303 summary.push(['engine', 'Not compatible with your version of node/npm: ' + er.pkgid])
304 detail.push(['notsup', [
305 'Not compatible with your version of node/npm: ' + er.pkgid,
306 'Required: ' + JSON.stringify(er.required),
307 'Actual: ' +
308 JSON.stringify({ node: process.version, npm: npm.version }),
309 ].join('\n')])
310 break
311
312 case 'ENOSPC':
313 summary.push(['nospc', er.message])
314 detail.push(['nospc', [
315 'There appears to be insufficient space on your system to finish.',
316 'Clear up some disk space and try again.',
317 ].join('\n')])
318 break
319
320 case 'EROFS':
321 summary.push(['rofs', er.message])
322 detail.push(['rofs', [
323 `Often virtualized file systems, or other file systems that don't support symlinks, give this error.`,
324 ].join('\n')])
325 break
326
327 case 'ENOENT':
328 summary.push(['enoent', er.message])
329 detail.push(['enoent', [
330 'This is related to npm not being able to find a file.',
331 er.file ? `\nCheck if the file '${er.file}' is present.` : '',
332 ].join('\n')])
333 break
334
335 case 'EMISSINGARG':
336 case 'EUNKNOWNTYPE':
337 case 'EINVALIDTYPE':
338 case 'ETOOMANYARGS':
339 summary.push(['typeerror', er.stack])
340 detail.push(['typeerror', [
341 'This is an error with npm itself. Please report this error at:',
342 'https://github.com/npm/cli/issues',
343 ].join('\n')])
344 break
345
346 default:
347 summary.push(['', er.message || er])
348 if (er.cause) {
349 detail.push(['cause', er.cause.message])
350 }
351 if (er.signal) {
352 detail.push(['signal', er.signal])
353 }
354 if (er.cmd && Array.isArray(er.args)) {
355 detail.push(['command', ...[er.cmd, ...er.args.map(replaceInfo)]])
356 }
357 if (er.stdout) {
358 detail.push(['', er.stdout.trim()])
359 }
360 if (er.stderr) {
361 detail.push(['', er.stderr.trim()])
362 }
363 break
364 }
365
366 return {
367 summary,
368 detail,
369 files,
370 json,
371 }
372}
373
374const getExitCodeFromError = (err) => {
375 if (typeof err?.errno === 'number') {
376 return err.errno
377 } else if (typeof err?.code === 'number') {
378 return err.code
379 }
380}
381
382const getError = (err, { npm, command, pkg }) => {
383 // if we got a command that just shells out to something else, then it will presumably print its own errors and exit with a proper status code if there's a problem.
384 // If we got an error with a code=0, then... something else went wrong along the way, so maybe an npm problem?
385 if (command?.constructor?.isShellout && typeof err.code === 'number' && err.code) {
386 return {
387 exitCode: err.code,
388 suppressError: true,
389 }
390 }
391
392 // XXX: we should stop throwing strings
393 if (typeof err === 'string') {
394 return {
395 exitCode: 1,
396 suppressError: true,
397 summary: [['', err]],
398 }
399 }
400
401 // XXX: we should stop throwing other non-errors
402 if (!(err instanceof Error)) {
403 return {
404 exitCode: 1,
405 suppressError: true,
406 summary: [['weird error', err]],
407 }
408 }
409
410 if (err.code === 'EUNKNOWNCOMMAND') {
411 const suggestions = require('./did-you-mean.js')(pkg, err.command)
412 return {
413 exitCode: 1,
414 suppressError: true,
415 standard: [
416 `Unknown command: "${err.command}"`,
417 suggestions,
418 'To see a list of supported npm commands, run:',
419 ' npm help',
420 ],
421 }
422 }
423
424 // Anything after this is not suppressed and gets more logged information
425
426 // add a code to the error if it doesnt have one and mutate some properties so they have redacted information
427 err.code ??= err.message.match(/^(?:Error: )?(E[A-Z]+)/)?.[1]
428 // this mutates the error and redacts stack/message
429 const { summary, detail, files, json } = errorMessage(err, npm)
430
431 return {
432 err,
433 code: err.code,
434 exitCode: getExitCodeFromError(err) || 1,
435 suppressError: false,
436 summary,
437 detail,
438 files,
439 json,
440 verbose: ['type', 'stack', 'statusCode', 'pkgid']
441 .filter(k => err[k])
442 .map(k => [k, replaceInfo(err[k])]),
443 error: ['code', 'syscall', 'file', 'path', 'dest', 'errno']
444 .filter(k => err[k])
445 .map(k => [k, err[k]]),
446 }
447}
448
449module.exports = {
450 getExitCodeFromError,
451 errorMessage,
452 getError,
453}
454 