UNPKG

npm

Version:

a package manager for JavaScript

221 lines (195 loc) 7.88 kB
// a tree representing the difference between two trees // A Diff node's parent is not necessarily the parent of // the node location it refers to, but rather the highest level // node that needs to be either changed or removed. // Thus, the root Diff node is the shallowest change required // for a given branch of the tree being mutated. const {depth} = require('treeverse') const {existsSync} = require('fs') const ssri = require('ssri') class Diff { constructor ({actual, ideal, filterSet}) { this.filterSet = filterSet this.children = [] this.actual = actual this.ideal = ideal if (this.ideal) { this.resolved = this.ideal.resolved this.integrity = this.ideal.integrity } this.action = getAction(this) this.parent = null // the set of leaf nodes that we rake up to the top level this.leaves = [] // the set of nodes that don't change in this branch of the tree this.unchanged = [] // the set of nodes that will be removed in this branch of the tree this.removed = [] } static calculate ({actual, ideal, filterNodes = []}) { // if there's a filterNode, then: // - get the path from the root to the filterNode. The root or // root.target should have an edge either to the filterNode or // a link to the filterNode. If not, abort. Add the path to the // filterSet. // - Add set of Nodes depended on by the filterNode to filterSet. // - Anything outside of that set should be ignored by getChildren const filterSet = new Set() for (const filterNode of filterNodes) { const { root } = filterNode if (root !== ideal && root !== actual) throw new Error('invalid filterNode: outside idealTree/actualTree') const { target } = root const rootTarget = target || root const edge = [...rootTarget.edgesOut.values()].filter(e => { return e.to && (e.to === filterNode || e.to.target === filterNode) })[0] filterSet.add(root) filterSet.add(rootTarget) filterSet.add(ideal) filterSet.add(actual) if (edge && edge.to) { filterSet.add(edge.to) if (edge.to.target) filterSet.add(edge.to.target) } filterSet.add(filterNode) depth({ tree: filterNode, visit: node => filterSet.add(node), getChildren: node => { node = node.target || node const loc = node.location const idealNode = ideal.inventory.get(loc) const ideals = !idealNode ? [] : [...idealNode.edgesOut.values()].filter(e => e.to).map(e => e.to) const actualNode = actual.inventory.get(loc) const actuals = !actualNode ? [] : [...actualNode.edgesOut.values()].filter(e => e.to).map(e => e.to) return ideals.concat(actuals) }, }) } return depth({ tree: new Diff({actual, ideal, filterSet}), getChildren, leave, }) } } const getAction = ({actual, ideal}) => { if (!ideal) return 'REMOVE' // bundled meta-deps are copied over to the ideal tree when we visit it, // so they'll appear to be missing here. There's no need to handle them // in the diff, though, because they'll be replaced at reify time anyway // Otherwise, add the missing node. if (!actual) return ideal.inDepBundle ? null : 'ADD' // always ignore the root node if (ideal.isRoot && actual.isRoot) return null const binsExist = ideal.binPaths.every((path) => existsSync(path)) // top nodes, links, and git deps won't have integrity, but do have resolved if (!ideal.integrity && !actual.integrity && ideal.resolved === actual.resolved && binsExist) return null // otherwise, verify that it's the same bits // note that if ideal has integrity, and resolved doesn't, we treat // that as a 'change', so that it gets re-fetched and locked down. if (!ideal.integrity || !actual.integrity || !ssri.parse(ideal.integrity).match(actual.integrity) || !binsExist) return 'CHANGE' return null } const allChildren = node => { if (!node) return new Map() // if the node is a global root, and also a link, then what we really // want is to traverse the target's children if (node.global && node.isRoot && node.isLink) return allChildren(node.target) const kids = new Map() for (const n of [node, ...node.fsChildren]) { for (const kid of n.children.values()) kids.set(kid.path, kid) } return kids } // functions for the walk options when we traverse the trees // to create the diff tree const getChildren = diff => { const children = [] const {actual, ideal, unchanged, removed, filterSet} = diff // Note: we DON'T diff fsChildren themselves, because they are either // included in the package contents, or part of some other project, and // will never appear in legacy shrinkwraps anyway. but we _do_ include the // child nodes of fsChildren, because those are nodes that we are typically // responsible for installing. const actualKids = allChildren(actual) const idealKids = allChildren(ideal) const paths = new Set([...actualKids.keys(), ...idealKids.keys()]) for (const path of paths) { const actual = actualKids.get(path) const ideal = idealKids.get(path) diffNode(actual, ideal, children, unchanged, removed, filterSet) } if (diff.leaves && !children.length) diff.leaves.push(diff) return children } const diffNode = (actual, ideal, children, unchanged, removed, filterSet) => { if (filterSet.size && !(filterSet.has(ideal) || filterSet.has(actual))) return const action = getAction({actual, ideal}) // if it's a match, then get its children // otherwise, this is the child diff node if (action) { if (action === 'REMOVE') removed.push(actual) children.push(new Diff({actual, ideal, filterSet})) } else { unchanged.push(ideal) // !*! Weird dirty hack warning !*! // // Bundled deps aren't loaded in the ideal tree, because we don't know // what they are going to be without unpacking. Swap them over now if // the bundling node isn't changing, so we don't prune them later. // // It's a little bit dirty to be doing this here, since it means that // diffing trees can mutate them, but otherwise we have to walk over // all unchanging bundlers and correct the diff later, so it's more // efficient to just fix it while we're passing through already. // // Note that moving over a bundled dep will break the links to other // deps under this parent, which may have been transitively bundled. // Breaking those links means that we'll no longer see the transitive // dependency, meaning that it won't appear as bundled any longer! // In order to not end up dropping transitively bundled deps, we have // to get the list of nodes to move, then move them all at once, rather // than moving them one at a time in the first loop. const bd = ideal.package.bundleDependencies if (actual && bd && bd.length) { const bundledChildren = [] for (const node of actual.children.values()) { if (node.inBundle) bundledChildren.push(node) } for (const node of bundledChildren) node.parent = ideal } children.push(...getChildren({actual, ideal, unchanged, removed, filterSet})) } } // set the parentage in the leave step so that we aren't attaching // child nodes only to remove them later. also bubble up the unchanged // nodes so that we can move them out of staging in the reification step. const leave = (diff, children) => { children.forEach(kid => { kid.parent = diff diff.leaves.push(...kid.leaves) diff.unchanged.push(...kid.unchanged) diff.removed.push(...kid.removed) }) diff.children = children return diff } module.exports = Diff