Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
error-message.js454 linesDownload Raw Back to utils
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