Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Isaacs/eresolve explanation #1761

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/outdated.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ async function outdated_ (tree, deps, opts) {
try {
const packument = await getPackument(spec)
const expected = edge.spec
// if it's not a range, version, or tag, skip it
if (!npa(`${edge.name}@${edge.spec}`).registry) {
return null
}
const wanted = pickManifest(packument, expected, npm.flatOptions)
const latest = pickManifest(packument, '*', npm.flatOptions)

Expand Down
7 changes: 7 additions & 0 deletions lib/utils/error-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.'])
Expand Down
148 changes: 148 additions & 0 deletions lib/utils/explain-eresolve.js
Original file line number Diff line number Diff line change
@@ -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
}
17 changes: 17 additions & 0 deletions lib/utils/setup-log.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
2 changes: 0 additions & 2 deletions node_modules/@npmcli/arborist/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading