// Given a dep, a node that depends on it, and the edge representing that
|
// dependency, place the dep somewhere in the node's tree, and all of its
|
// peer dependencies.
|
//
|
// Handles all of the tree updating needed to place the dep, including
|
// removing replaced nodes, pruning now-extraneous or invalidated nodes,
|
// and saves a set of what was placed and what needs re-evaluation as
|
// a result.
|
|
const localeCompare = require('@isaacs/string-locale-compare')('en')
|
const { log } = require('proc-log')
|
const { redact } = require('@npmcli/redact')
|
const deepestNestingTarget = require('./deepest-nesting-target.js')
|
const CanPlaceDep = require('./can-place-dep.js')
|
const {
|
KEEP,
|
CONFLICT,
|
} = CanPlaceDep
|
const debug = require('./debug.js')
|
|
const Link = require('./link.js')
|
const gatherDepSet = require('./gather-dep-set.js')
|
const peerEntrySets = require('./peer-entry-sets.js')
|
|
class PlaceDep {
|
constructor (options) {
|
this.auditReport = options.auditReport
|
this.dep = options.dep
|
this.edge = options.edge
|
this.explicitRequest = options.explicitRequest
|
this.force = options.force
|
this.installLinks = options.installLinks
|
this.installStrategy = options.installStrategy
|
this.legacyPeerDeps = options.legacyPeerDeps
|
this.parent = options.parent || null
|
this.preferDedupe = options.preferDedupe
|
this.strictPeerDeps = options.strictPeerDeps
|
this.updateNames = options.updateNames
|
|
this.canPlace = null
|
this.canPlaceSelf = null
|
// XXX this only appears to be used by tests
|
this.checks = new Map()
|
this.children = []
|
this.needEvaluation = new Set()
|
this.peerConflict = null
|
this.placed = null
|
this.target = null
|
|
this.current = this.edge.to
|
this.name = this.edge.name
|
this.top = this.parent?.top || this
|
|
// nothing to do if the edge is fine as it is
|
if (this.edge.to &&
|
!this.edge.error &&
|
!this.explicitRequest &&
|
!this.updateNames.includes(this.edge.name) &&
|
!this.auditReport?.isVulnerable(this.edge.to)) {
|
return
|
}
|
|
// walk up the tree until we hit either a top/root node, or a place
|
// where the dep is not a peer dep.
|
const start = this.getStartNode()
|
|
for (const target of start.ancestry()) {
|
// if the current location has a peerDep on it, then we can't place here
|
// this is pretty rare to hit, since we always prefer deduping peers,
|
// and the getStartNode will start us out above any peers from the
|
// thing that depends on it. but we could hit it with something like:
|
//
|
// a -> (b@1, c@1)
|
// +-- c@1
|
// +-- b -> PEEROPTIONAL(v) (c@2)
|
// +-- c@2 -> (v)
|
//
|
// So we check if we can place v under c@2, that's fine.
|
// Then we check under b, and can't, because of the optional peer dep.
|
// but we CAN place it under a, so the correct thing to do is keep
|
// walking up the tree.
|
const targetEdge = target.edgesOut.get(this.edge.name)
|
if (!target.isTop && targetEdge && targetEdge.peer) {
|
continue
|
}
|
|
const cpd = new CanPlaceDep({
|
dep: this.dep,
|
edge: this.edge,
|
// note: this sets the parent's canPlace as the parent of this
|
// canPlace, but it does NOT add this canPlace to the parent's
|
// children. This way, we can know that it's a peer dep, and
|
// get the top edge easily, while still maintaining the
|
// tree of checks that factored into the original decision.
|
parent: this.parent && this.parent.canPlace,
|
target,
|
preferDedupe: this.preferDedupe,
|
explicitRequest: this.explicitRequest,
|
})
|
this.checks.set(target, cpd)
|
|
// It's possible that a "conflict" is a conflict among the *peers* of
|
// a given node we're trying to place, but there actually is no current
|
// node. Eg,
|
// root -> (a, b)
|
// a -> PEER(c)
|
// b -> PEER(d)
|
// d -> PEER(c@2)
|
// We place (a), and get a peer of (c) along with it.
|
// then we try to place (b), and get CONFLICT in the check, because
|
// of the conflicting peer from (b)->(d)->(c@2). In that case, we
|
// should treat (b) and (d) as OK, and place them in the last place
|
// where they did not themselves conflict, and skip c@2 if conflict
|
// is ok by virtue of being forced or not ours and not strict.
|
if (cpd.canPlaceSelf !== CONFLICT) {
|
this.canPlaceSelf = cpd
|
}
|
|
// we found a place this can go, along with all its peer friends.
|
// we break when we get the first conflict
|
if (cpd.canPlace !== CONFLICT) {
|
this.canPlace = cpd
|
} else {
|
break
|
}
|
|
// if it's a load failure, just plop it in the first place attempted,
|
// since we're going to crash the build or prune it out anyway.
|
// but, this will frequently NOT be a successful canPlace, because
|
// it'll have no version or other information.
|
if (this.dep.errors.length) {
|
break
|
}
|
|
// nest packages like npm v1 and v2
|
// very disk-inefficient
|
if (this.installStrategy === 'nested') {
|
break
|
}
|
|
// when installing globally, or just in global style, we never place
|
// deps above the first level.
|
if (this.installStrategy === 'shallow') {
|
const rp = target.resolveParent
|
if (rp && rp.isProjectRoot) {
|
break
|
}
|
}
|
}
|
|
// if we can't find a target, that means that the last place checked,
|
// and all the places before it, had a conflict.
|
if (!this.canPlace) {
|
// if not forced, and it's our dep, or strictPeerDeps is set, then
|
// this is an ERESOLVE error.
|
if (!this.force && (this.isMine || this.strictPeerDeps)) {
|
return this.failPeerConflict()
|
}
|
|
// ok! we're gonna allow the conflict, but we should still warn
|
// if we have a current, then we treat CONFLICT as a KEEP.
|
// otherwise, we just skip it. Only warn on the one that actually
|
// could not be placed somewhere.
|
if (!this.canPlaceSelf) {
|
this.warnPeerConflict()
|
return
|
}
|
|
this.canPlace = this.canPlaceSelf
|
}
|
|
// now we have a target, a tree of CanPlaceDep results for the peer group,
|
// and we are ready to go
|
|
/* istanbul ignore next */
|
if (!this.canPlace) {
|
debug(() => {
|
throw new Error('canPlace not set, but trying to place in tree')
|
})
|
return
|
}
|
|
const { target } = this.canPlace
|
|
log.silly(
|
'placeDep',
|
target.location || 'ROOT',
|
`${this.dep.name}@${this.dep.version}`,
|
this.canPlace.description,
|
`for: ${this.edge.from.package._id || this.edge.from.location}`,
|
`want: ${redact(this.edge.spec || '*')}`
|
)
|
|
const placementType = this.canPlace.canPlace === CONFLICT
|
? this.canPlace.canPlaceSelf
|
: this.canPlace.canPlace
|
|
// if we're placing in the tree with --force, we can get here even though
|
// it's a conflict. Treat it as a KEEP, but warn and move on.
|
if (placementType === KEEP) {
|
// this was a peerConflicted peer dep
|
if (this.edge.peer && !this.edge.valid) {
|
this.warnPeerConflict()
|
}
|
|
// if we get a KEEP in a update scenario, then we MAY have something
|
// already duplicating this unnecessarily! For example:
|
// ```
|
// root (dep: y@1)
|
// +-- x (dep: y@1.1)
|
// | +-- y@1.1.0 (replacing with 1.1.2, got KEEP at the root)
|
// +-- y@1.1.2 (updated already from 1.0.0)
|
// ```
|
// Now say we do `reify({update:['y']})`, and the latest version is
|
// 1.1.2, which we now have in the root. We'll try to place y@1.1.2
|
// first in x, then in the root, ending with KEEP, because we already
|
// have it. In that case, we ought to REMOVE the nm/x/nm/y node, because
|
// it is an unnecessary duplicate.
|
this.pruneDedupable(target)
|
return
|
}
|
|
// we were told to place it here in the target, so either it does not
|
// already exist in the tree, OR it's shadowed.
|
// handle otherwise unresolvable dependency nesting loops by
|
// creating a symbolic link
|
// a1 -> b1 -> a2 -> b2 -> a1 -> ...
|
// instead of nesting forever, when the loop occurs, create
|
// a symbolic link to the earlier instance
|
for (let p = target; p; p = p.resolveParent) {
|
if (p.matches(this.dep) && !p.isTop) {
|
this.placed = new Link({ parent: target, target: p })
|
return
|
}
|
}
|
|
// XXX if we are replacing SOME of a peer entry group, we will need to
|
// remove any that are not being replaced and will now be invalid, and
|
// re-evaluate them deeper into the tree.
|
|
const virtualRoot = this.dep.parent
|
this.placed = new this.dep.constructor({
|
name: this.dep.name,
|
pkg: this.dep.package,
|
resolved: this.dep.resolved,
|
integrity: this.dep.integrity,
|
installLinks: this.installLinks,
|
legacyPeerDeps: this.legacyPeerDeps,
|
error: this.dep.errors[0],
|
...(this.dep.overrides ? { overrides: this.dep.overrides } : {}),
|
...(this.dep.isLink ? { target: this.dep.target, realpath: this.dep.realpath } : {}),
|
})
|
|
this.oldDep = target.children.get(this.name)
|
if (this.oldDep) {
|
this.replaceOldDep()
|
} else {
|
this.placed.parent = target
|
}
|
|
// if it's a peerConflicted peer dep, warn about it
|
if (this.edge.peer && !this.placed.satisfies(this.edge)) {
|
this.warnPeerConflict()
|
}
|
|
// If the edge is not an error, then we're updating something, and
|
// MAY end up putting a better/identical node further up the tree in
|
// a way that causes an unnecessary duplication. If so, remove the
|
// now-unnecessary node.
|
if (this.edge.valid && this.edge.to && this.edge.to !== this.placed) {
|
this.pruneDedupable(this.edge.to, false)
|
}
|
|
// in case we just made some duplicates that can be removed,
|
// prune anything deeper in the tree that can be replaced by this
|
for (const node of target.root.inventory.query('name', this.name)) {
|
if (node.isDescendantOf(target) && !node.isTop) {
|
this.pruneDedupable(node, false)
|
// only walk the direct children of the ones we kept
|
if (node.root === target.root) {
|
for (const kid of node.children.values()) {
|
this.pruneDedupable(kid, false)
|
}
|
}
|
}
|
}
|
|
// also place its unmet or invalid peer deps at this location
|
// loop through any peer deps from the thing we just placed, and place
|
// those ones as well. it's safe to do this with the virtual nodes,
|
// because we're copying rather than moving them out of the virtual root,
|
// otherwise they'd be gone and the peer set would change throughout
|
// this loop.
|
for (const peerEdge of this.placed.edgesOut.values()) {
|
if (peerEdge.valid || !peerEdge.peer || peerEdge.peerConflicted) {
|
continue
|
}
|
|
const peer = virtualRoot.children.get(peerEdge.name)
|
|
// Note: if the virtualRoot *doesn't* have the peer, then that means
|
// it's an optional peer dep. If it's not being properly met (ie,
|
// peerEdge.valid is false), then this is likely heading for an
|
// ERESOLVE error, unless it can walk further up the tree.
|
if (!peer) {
|
continue
|
}
|
|
// peerConflicted peerEdge, just accept what's there already
|
if (!peer.satisfies(peerEdge)) {
|
continue
|
}
|
|
this.children.push(new PlaceDep({
|
auditReport: this.auditReport,
|
explicitRequest: this.explicitRequest,
|
force: this.force,
|
installLinks: this.installLinks,
|
installStrategy: this.installStrategy,
|
legacyPeerDeps: this.legaycPeerDeps,
|
preferDedupe: this.preferDedupe,
|
strictPeerDeps: this.strictPeerDeps,
|
updateNames: this.updateName,
|
parent: this,
|
dep: peer,
|
node: this.placed,
|
edge: peerEdge,
|
}))
|
}
|
}
|
|
replaceOldDep () {
|
const target = this.oldDep.parent
|
|
// XXX handle replacing an entire peer group?
|
// what about cases where we need to push some other peer groups deeper
|
// into the tree? all the tree updating should be done here, and track
|
// all the things that we add and remove, so that we can know what
|
// to re-evaluate.
|
|
// if we're replacing, we should also remove any nodes for edges that
|
// are now invalid, and where this (or its deps) is the only dependent,
|
// and also recurse on that pruning. Otherwise leaving that dep node
|
// around can result in spurious conflicts pushing nodes deeper into
|
// the tree than needed in the case of cycles that will be removed
|
// later anyway.
|
const oldDeps = []
|
for (const [name, edge] of this.oldDep.edgesOut.entries()) {
|
if (!this.placed.edgesOut.has(name) && edge.to) {
|
oldDeps.push(...gatherDepSet([edge.to], e => e.to !== edge.to))
|
}
|
}
|
|
// gather all peer edgesIn which are at this level, and will not be
|
// satisfied by the new dependency. Those are the peer sets that need
|
// to be either warned about (if they cannot go deeper), or removed and
|
// re-placed (if they can).
|
const prunePeerSets = []
|
for (const edge of this.oldDep.edgesIn) {
|
if (this.placed.satisfies(edge) ||
|
!edge.peer ||
|
edge.from.parent !== target ||
|
edge.peerConflicted) {
|
// not a peer dep, not invalid, or not from this level, so it's fine
|
// to just let it re-evaluate as a problemEdge later, or let it be
|
// satisfied by the new dep being placed.
|
continue
|
}
|
for (const entryEdge of peerEntrySets(edge.from).keys()) {
|
// either this one needs to be pruned and re-evaluated, or marked
|
// as peerConflicted and warned about. If the entryEdge comes in from
|
// the root or a workspace, then we have to leave it alone, and in that
|
// case, it will have already warned or crashed by getting to this point
|
const entryNode = entryEdge.to
|
const deepestTarget = deepestNestingTarget(entryNode)
|
if (deepestTarget !== target &&
|
!(entryEdge.from.isProjectRoot || entryEdge.from.isWorkspace)) {
|
prunePeerSets.push(...gatherDepSet([entryNode], e => {
|
return e.to !== entryNode && !e.peerConflicted
|
}))
|
} else {
|
this.warnPeerConflict(edge, this.dep)
|
}
|
}
|
}
|
|
this.placed.replace(this.oldDep)
|
this.pruneForReplacement(this.placed, oldDeps)
|
for (const dep of prunePeerSets) {
|
for (const edge of dep.edgesIn) {
|
this.needEvaluation.add(edge.from)
|
}
|
dep.root = null
|
}
|
}
|
|
pruneForReplacement (node, oldDeps) {
|
// gather up all the now-invalid/extraneous edgesOut, as long as they are
|
// only depended upon by the old node/deps
|
const invalidDeps = new Set([...node.edgesOut.values()]
|
.filter(e => e.to && !e.valid).map(e => e.to))
|
for (const dep of oldDeps) {
|
const set = gatherDepSet([dep], e => e.to !== dep && e.valid)
|
for (const dep of set) {
|
invalidDeps.add(dep)
|
}
|
}
|
|
// ignore dependency edges from the node being replaced, but
|
// otherwise filter the set down to just the set with no
|
// dependencies from outside the set, except the node in question.
|
const deps = gatherDepSet(invalidDeps, edge =>
|
edge.from !== node && edge.to !== node && edge.valid)
|
|
// now just delete whatever's left, because it's junk
|
for (const dep of deps) {
|
dep.root = null
|
}
|
}
|
|
// prune all the nodes in a branch of the tree that can be safely removed
|
// This is only the most basic duplication detection; it finds if there
|
// is another satisfying node further up the tree, and if so, dedupes.
|
// Even in installStategy is nested, we do this amount of deduplication.
|
pruneDedupable (node, descend = true) {
|
if (node.canDedupe(this.preferDedupe)) {
|
// gather up all deps that have no valid edges in from outside
|
// the dep set, except for this node we're deduping, so that we
|
// also prune deps that would be made extraneous.
|
const deps = gatherDepSet([node], e => e.to !== node && e.valid)
|
for (const node of deps) {
|
node.root = null
|
}
|
return
|
}
|
if (descend) {
|
// sort these so that they're deterministically ordered
|
// otherwise, resulting tree shape is dependent on the order
|
// in which they happened to be resolved.
|
const nodeSort = (a, b) => localeCompare(a.location, b.location)
|
|
const children = [...node.children.values()].sort(nodeSort)
|
for (const child of children) {
|
this.pruneDedupable(child)
|
}
|
const fsChildren = [...node.fsChildren].sort(nodeSort)
|
for (const topNode of fsChildren) {
|
const children = [...topNode.children.values()].sort(nodeSort)
|
for (const child of children) {
|
this.pruneDedupable(child)
|
}
|
}
|
}
|
}
|
|
get isMine () {
|
const { edge } = this.top
|
const { from: node } = edge
|
|
if (node.isWorkspace || node.isProjectRoot) {
|
return true
|
}
|
|
if (!edge.peer) {
|
return false
|
}
|
|
// re-entry case. check if any non-peer edges come from the project,
|
// or any entryEdges on peer groups are from the root.
|
let hasPeerEdges = false
|
for (const edge of node.edgesIn) {
|
if (edge.peer) {
|
hasPeerEdges = true
|
continue
|
}
|
if (edge.from.isWorkspace || edge.from.isProjectRoot) {
|
return true
|
}
|
}
|
if (hasPeerEdges) {
|
for (const edge of peerEntrySets(node).keys()) {
|
if (edge.from.isWorkspace || edge.from.isProjectRoot) {
|
return true
|
}
|
}
|
}
|
|
return false
|
}
|
|
warnPeerConflict (edge, dep) {
|
edge = edge || this.edge
|
dep = dep || this.dep
|
edge.peerConflicted = true
|
const expl = this.explainPeerConflict(edge, dep)
|
log.warn('ERESOLVE', 'overriding peer dependency', expl)
|
}
|
|
failPeerConflict (edge, dep) {
|
edge = edge || this.top.edge
|
dep = dep || this.top.dep
|
const expl = this.explainPeerConflict(edge, dep)
|
throw Object.assign(new Error('could not resolve'), expl)
|
}
|
|
explainPeerConflict (edge, dep) {
|
const { from: node } = edge
|
const curNode = node.resolve(edge.name)
|
|
// XXX decorate more with this.canPlace and this.canPlaceSelf,
|
// this.checks, this.children, walk over conflicted peers, etc.
|
const expl = {
|
code: 'ERESOLVE',
|
edge: edge.explain(),
|
dep: dep.explain(edge),
|
force: this.force,
|
isMine: this.isMine,
|
strictPeerDeps: this.strictPeerDeps,
|
}
|
|
if (this.parent) {
|
// this is the conflicted peer
|
expl.current = curNode && curNode.explain(edge)
|
expl.peerConflict = this.current && this.current.explain(this.edge)
|
} else {
|
expl.current = curNode && curNode.explain()
|
if (this.canPlaceSelf && this.canPlaceSelf.canPlaceSelf !== CONFLICT) {
|
// failed while checking for a child dep
|
const cps = this.canPlaceSelf
|
for (const peer of cps.conflictChildren) {
|
if (peer.current) {
|
expl.peerConflict = {
|
current: peer.current.explain(),
|
peer: peer.dep.explain(peer.edge),
|
}
|
break
|
}
|
}
|
} else {
|
expl.peerConflict = {
|
current: this.current && this.current.explain(),
|
peer: this.dep.explain(this.edge),
|
}
|
}
|
}
|
|
return expl
|
}
|
|
getStartNode () {
|
// if we are a peer, then we MUST be at least as shallow as the peer
|
// dependent
|
const from = this.parent?.getStartNode() || this.edge.from
|
return deepestNestingTarget(from, this.name)
|
}
|
|
// XXX this only appears to be used by tests
|
get allChildren () {
|
const set = new Set(this.children)
|
for (const child of set) {
|
for (const grandchild of child.children) {
|
set.add(grandchild)
|
}
|
}
|
return [...set]
|
}
|
}
|
|
module.exports = PlaceDep
|