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