관리-도구
편집 파일: place-dep.js
// 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