|
| 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 | +} |
0 commit comments