Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/npm/cli.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib/utils
diff options
context:
space:
mode:
authorisaacs <i@izs.me>2020-09-04 04:22:42 +0300
committerisaacs <i@izs.me>2020-09-04 21:51:33 +0300
commit2a4e2e9efecb7f86147e5071c59cfc2461a5a7f5 (patch)
tree3c51f6083502b161c62069505d2ab2730f1f30c7 /lib/utils
parent371f0f06215ad8caf598c20e3d0d38ff597531e9 (diff)
Explain ERESOLVE errors
When peerDependencies conflict, Arborist is now providing details of the nodes and their reasons for inclusion on the Error object, including whether or not this resolution error could be overridden using the --force flag. Print this data out in a minimal way as a warning if we override an ERESOLVE forcefully. When the ERESOLVE causes the install to fail, print a somewhat longer message, and save a MUCH longer full report to the cache folder. PR-URL: https://github.com/npm/cli/pull/1761 Credit: @isaacs Close: #1761 Reviewed-by: @darcyclarke, @ruyadorno
Diffstat (limited to 'lib/utils')
-rw-r--r--lib/utils/error-message.js7
-rw-r--r--lib/utils/explain-eresolve.js148
-rw-r--r--lib/utils/setup-log.js17
3 files changed, 172 insertions, 0 deletions
diff --git a/lib/utils/error-message.js b/lib/utils/error-message.js
index fde793fc4..7f9aca95e 100644
--- a/lib/utils/error-message.js
+++ b/lib/utils/error-message.js
@@ -4,11 +4,18 @@ const { format } = require('util')
const { resolve } = require('path')
const nameValidator = require('validate-npm-package-name')
const npmlog = require('npmlog')
+const { report: explainEresolve } = require('./explain-eresolve.js')
module.exports = (er) => {
const short = []
const detail = []
switch (er.code) {
+ case 'ERESOLVE':
+ short.push(['ERESOLVE', er.message])
+ detail.push(['', ''])
+ detail.push(['', explainEresolve(er)])
+ break
+
case 'ENOLOCK': {
const cmd = npm.command || ''
short.push([cmd, 'This command requires an existing lockfile.'])
diff --git a/lib/utils/explain-eresolve.js b/lib/utils/explain-eresolve.js
new file mode 100644
index 000000000..65a62a115
--- /dev/null
+++ b/lib/utils/explain-eresolve.js
@@ -0,0 +1,148 @@
+// this is called when an ERESOLVE error is caught in the error-handler,
+// or when there's a log.warn('eresolve', msg, explanation), to turn it
+// into a human-intelligible explanation of what's wrong and how to fix.
+//
+// TODO: abstract out the explainNode methods into a separate util for
+// use by a future `npm explain <path || spec>` command.
+
+const npm = require('../npm.js')
+const { writeFileSync } = require('fs')
+const { resolve } = require('path')
+
+const chalk = require('chalk')
+const nocolor = {
+ bold: s => s,
+ dim: s => s
+}
+
+// expl is an explanation object that comes from Arborist. It looks like:
+// {
+// dep: {
+// whileInstalling: {
+// explanation of the thing being installed when we hit the conflict
+// },
+// name,
+// version,
+// dependents: [
+// things depending on this node (ie, reason for inclusion)
+// { name, version, dependents }, ...
+// ]
+// }
+// current: {
+// explanation of the current node that already was in the tree conflicting
+// }
+// peerConflict: {
+// explanation of the peer dependency that couldn't be added, or null
+// }
+// fixWithForce: Boolean - can we use --force to push through this?
+// type: type of the edge that couldn't be met
+// isPeer: true if the edge that couldn't be met is a peer dependency
+// }
+// Depth is how far we want to want to descend into the object making a report.
+// The full report (ie, depth=Infinity) is always written to the cache folder
+// at ${cache}/eresolve-report.txt along with full json.
+const explainEresolve = (expl, color, depth) => {
+ const { dep, current, peerConflict } = expl
+
+ const out = []
+ /* istanbul ignore else - should always have this for ERESOLVEs */
+ if (dep.whileInstalling) {
+ out.push('While resolving: ' + printNode(dep.whileInstalling, color))
+ }
+
+ out.push(explainNode('Found:', current, depth, color))
+
+ out.push(explainNode('\nCould not add conflicting dependency:', dep, depth, color))
+
+ if (peerConflict) {
+ const heading = '\nConflicting peer dependency:'
+ const pc = explainNode(heading, peerConflict, depth, color)
+ out.push(pc)
+ }
+
+ return out.join('\n')
+}
+
+const explainNode = (heading, node, depth, color) =>
+ `${heading} ${printNode(node, color)}` +
+ explainDependents(node, depth, color)
+
+const printNode = ({ name, version, location }, color) => {
+ const { bold, dim } = color ? chalk : nocolor
+ return `${bold(name)}@${bold(version)}` +
+ (location ? dim(` at ${location}`) : '')
+}
+
+const explainDependents = ({ name, dependents }, depth, color) => {
+ if (!dependents || !dependents.length || depth <= 0) {
+ return ''
+ }
+
+ const max = Math.ceil(depth / 2)
+ const messages = dependents.slice(0, max)
+ .map(dep => explainDependency(name, dep, depth, color))
+
+ // show just the names of the first 5 deps that overflowed the list
+ if (dependents.length > max) {
+ const names = dependents.slice(max).map(d => d.from.name)
+ const showNames = names.slice(0, 5)
+ if (showNames.length < names.length) {
+ showNames.push('...')
+ }
+ const show = `(${showNames.join(', ')})`
+ messages.push(`${names.length} more ${show}`)
+ }
+
+ const str = '\nfor: ' + messages.join('\nand: ')
+ return str.split('\n').join('\n ')
+}
+
+const explainDependency = (name, { type, from, spec }, depth, color) => {
+ const { bold } = color ? chalk : nocolor
+ return `${type} dependency ` +
+ `${bold(name)}@"${bold(spec)}"\nfrom: ` +
+ explainFrom(from, depth, color)
+}
+
+const explainFrom = (from, depth, color) => {
+ if (!from.name && !from.version) {
+ return 'the root project'
+ }
+
+ return printNode(from, color) +
+ explainDependents(from, depth - 1, color)
+}
+
+// generate a full verbose report and tell the user how to fix it
+const report = (expl, depth = 4) => {
+ const fullReport = resolve(npm.cache, 'eresolve-report.txt')
+
+ const orForce = expl.fixWithForce ? ' or --force' : ''
+ const fix = `Fix the upstream dependency conflict, or retry
+this command with --legacy-peer-deps${orForce}
+to accept an incorrect (and potentially broken) dependency resolution.`
+
+ writeFileSync(fullReport, `# npm resolution error report
+
+${new Date().toISOString()}
+
+${explainEresolve(expl, false, Infinity)}
+
+${fix}
+
+Raw JSON explanation object:
+
+${JSON.stringify(expl, null, 2)}
+`, 'utf8')
+
+ return explainEresolve(expl, npm.color, depth) +
+ `\n\n${fix}\n\nSee ${fullReport} for a full report.`
+}
+
+// the terser explain method for the warning when using --force
+const explain = (expl, depth = 2) => explainEresolve(expl, npm.color, depth)
+
+module.exports = {
+ explain,
+ report
+}
diff --git a/lib/utils/setup-log.js b/lib/utils/setup-log.js
index 9e845de0e..dde55b4fa 100644
--- a/lib/utils/setup-log.js
+++ b/lib/utils/setup-log.js
@@ -1,10 +1,27 @@
// module to set the appropriate log settings based on configs
// returns a boolean to say whether we should enable color on
// stdout or not.
+//
+// Also (and this is a really inexcusable kludge), we patch the
+// log.warn() method so that when we see a peerDep override
+// explanation from Arborist, we can replace the object with a
+// highly abbreviated explanation of what's being overridden.
const log = require('npmlog')
+const { explain } = require('./explain-eresolve.js')
+
module.exports = (config) => {
const color = config.get('color')
+ const { warn } = log
+
+ log.warn = (heading, ...args) => {
+ if (heading === 'ERESOLVE' && args[1] && typeof args[1] === 'object') {
+ warn(heading, args[0])
+ return warn('', explain(args[1]))
+ }
+ return warn(heading, ...args)
+ }
+
if (config.get('timing') && config.get('loglevel') === 'notice') {
log.level = 'timing'
} else {