Skip to content

Commit 2a4e2e9

Browse files
committed
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: #1761 Credit: @isaacs Close: #1761 Reviewed-by: @darcyclarke, @ruyadorno
1 parent 371f0f0 commit 2a4e2e9

9 files changed

Lines changed: 3891 additions & 24 deletions

lib/utils/error-message.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@ const { format } = require('util')
44
const { resolve } = require('path')
55
const nameValidator = require('validate-npm-package-name')
66
const npmlog = require('npmlog')
7+
const { report: explainEresolve } = require('./explain-eresolve.js')
78

89
module.exports = (er) => {
910
const short = []
1011
const detail = []
1112
switch (er.code) {
13+
case 'ERESOLVE':
14+
short.push(['ERESOLVE', er.message])
15+
detail.push(['', ''])
16+
detail.push(['', explainEresolve(er)])
17+
break
18+
1219
case 'ENOLOCK': {
1320
const cmd = npm.command || ''
1421
short.push([cmd, 'This command requires an existing lockfile.'])

lib/utils/explain-eresolve.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// this is called when an ERESOLVE error is caught in the error-handler,
2+
// or when there's a log.warn('eresolve', msg, explanation), to turn it
3+
// into a human-intelligible explanation of what's wrong and how to fix.
4+
//
5+
// TODO: abstract out the explainNode methods into a separate util for
6+
// use by a future `npm explain <path || spec>` command.
7+
8+
const npm = require('../npm.js')
9+
const { writeFileSync } = require('fs')
10+
const { resolve } = require('path')
11+
12+
const chalk = require('chalk')
13+
const nocolor = {
14+
bold: s => s,
15+
dim: s => s
16+
}
17+
18+
// expl is an explanation object that comes from Arborist. It looks like:
19+
// {
20+
// dep: {
21+
// whileInstalling: {
22+
// explanation of the thing being installed when we hit the conflict
23+
// },
24+
// name,
25+
// version,
26+
// dependents: [
27+
// things depending on this node (ie, reason for inclusion)
28+
// { name, version, dependents }, ...
29+
// ]
30+
// }
31+
// current: {
32+
// explanation of the current node that already was in the tree conflicting
33+
// }
34+
// peerConflict: {
35+
// explanation of the peer dependency that couldn't be added, or null
36+
// }
37+
// fixWithForce: Boolean - can we use --force to push through this?
38+
// type: type of the edge that couldn't be met
39+
// isPeer: true if the edge that couldn't be met is a peer dependency
40+
// }
41+
// Depth is how far we want to want to descend into the object making a report.
42+
// The full report (ie, depth=Infinity) is always written to the cache folder
43+
// at ${cache}/eresolve-report.txt along with full json.
44+
const explainEresolve = (expl, color, depth) => {
45+
const { dep, current, peerConflict } = expl
46+
47+
const out = []
48+
/* istanbul ignore else - should always have this for ERESOLVEs */
49+
if (dep.whileInstalling) {
50+
out.push('While resolving: ' + printNode(dep.whileInstalling, color))
51+
}
52+
53+
out.push(explainNode('Found:', current, depth, color))
54+
55+
out.push(explainNode('\nCould not add conflicting dependency:', dep, depth, color))
56+
57+
if (peerConflict) {
58+
const heading = '\nConflicting peer dependency:'
59+
const pc = explainNode(heading, peerConflict, depth, color)
60+
out.push(pc)
61+
}
62+
63+
return out.join('\n')
64+
}
65+
66+
const explainNode = (heading, node, depth, color) =>
67+
`${heading} ${printNode(node, color)}` +
68+
explainDependents(node, depth, color)
69+
70+
const printNode = ({ name, version, location }, color) => {
71+
const { bold, dim } = color ? chalk : nocolor
72+
return `${bold(name)}@${bold(version)}` +
73+
(location ? dim(` at ${location}`) : '')
74+
}
75+
76+
const explainDependents = ({ name, dependents }, depth, color) => {
77+
if (!dependents || !dependents.length || depth <= 0) {
78+
return ''
79+
}
80+
81+
const max = Math.ceil(depth / 2)
82+
const messages = dependents.slice(0, max)
83+
.map(dep => explainDependency(name, dep, depth, color))
84+
85+
// show just the names of the first 5 deps that overflowed the list
86+
if (dependents.length > max) {
87+
const names = dependents.slice(max).map(d => d.from.name)
88+
const showNames = names.slice(0, 5)
89+
if (showNames.length < names.length) {
90+
showNames.push('...')
91+
}
92+
const show = `(${showNames.join(', ')})`
93+
messages.push(`${names.length} more ${show}`)
94+
}
95+
96+
const str = '\nfor: ' + messages.join('\nand: ')
97+
return str.split('\n').join('\n ')
98+
}
99+
100+
const explainDependency = (name, { type, from, spec }, depth, color) => {
101+
const { bold } = color ? chalk : nocolor
102+
return `${type} dependency ` +
103+
`${bold(name)}@"${bold(spec)}"\nfrom: ` +
104+
explainFrom(from, depth, color)
105+
}
106+
107+
const explainFrom = (from, depth, color) => {
108+
if (!from.name && !from.version) {
109+
return 'the root project'
110+
}
111+
112+
return printNode(from, color) +
113+
explainDependents(from, depth - 1, color)
114+
}
115+
116+
// generate a full verbose report and tell the user how to fix it
117+
const report = (expl, depth = 4) => {
118+
const fullReport = resolve(npm.cache, 'eresolve-report.txt')
119+
120+
const orForce = expl.fixWithForce ? ' or --force' : ''
121+
const fix = `Fix the upstream dependency conflict, or retry
122+
this command with --legacy-peer-deps${orForce}
123+
to accept an incorrect (and potentially broken) dependency resolution.`
124+
125+
writeFileSync(fullReport, `# npm resolution error report
126+
127+
${new Date().toISOString()}
128+
129+
${explainEresolve(expl, false, Infinity)}
130+
131+
${fix}
132+
133+
Raw JSON explanation object:
134+
135+
${JSON.stringify(expl, null, 2)}
136+
`, 'utf8')
137+
138+
return explainEresolve(expl, npm.color, depth) +
139+
`\n\n${fix}\n\nSee ${fullReport} for a full report.`
140+
}
141+
142+
// the terser explain method for the warning when using --force
143+
const explain = (expl, depth = 2) => explainEresolve(expl, npm.color, depth)
144+
145+
module.exports = {
146+
explain,
147+
report
148+
}

lib/utils/setup-log.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
// module to set the appropriate log settings based on configs
22
// returns a boolean to say whether we should enable color on
33
// stdout or not.
4+
//
5+
// Also (and this is a really inexcusable kludge), we patch the
6+
// log.warn() method so that when we see a peerDep override
7+
// explanation from Arborist, we can replace the object with a
8+
// highly abbreviated explanation of what's being overridden.
49
const log = require('npmlog')
10+
const { explain } = require('./explain-eresolve.js')
11+
512
module.exports = (config) => {
613
const color = config.get('color')
714

15+
const { warn } = log
16+
17+
log.warn = (heading, ...args) => {
18+
if (heading === 'ERESOLVE' && args[1] && typeof args[1] === 'object') {
19+
warn(heading, args[0])
20+
return warn('', explain(args[1]))
21+
}
22+
return warn(heading, ...args)
23+
}
24+
825
if (config.get('timing') && config.get('loglevel') === 'notice') {
926
log.level = 'timing'
1027
} else {

tap-snapshots/test-lib-utils-error-message.js-TAP.test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,27 @@ Object {
782782
}
783783
`
784784

785+
exports[`test/lib/utils/error-message.js TAP explain ERESOLVE errors > must match snapshot 1`] = `
786+
Object {
787+
"detail": Array [
788+
Array [
789+
"",
790+
"",
791+
],
792+
Array [
793+
"",
794+
"explanation",
795+
],
796+
],
797+
"summary": Array [
798+
Array [
799+
"ERESOLVE",
800+
"could not resolve",
801+
],
802+
],
803+
}
804+
`
805+
785806
exports[`test/lib/utils/error-message.js TAP just simple messages > must match snapshot 1`] = `
786807
Object {
787808
"detail": Array [],

0 commit comments

Comments
 (0)