mirror of
https://github.com/nodejs/node.git
synced 2025-05-13 19:50:17 +00:00

PR-URL: https://github.com/nodejs/node/pull/40865 Reviewed-By: Michaël Zasso <targos@protonmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
307 lines
9.6 KiB
JavaScript
307 lines
9.6 KiB
JavaScript
// 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, shrinkwrapInflated }) {
|
|
this.filterSet = filterSet
|
|
this.shrinkwrapInflated = shrinkwrapInflated
|
|
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 = [],
|
|
shrinkwrapInflated = new Set(),
|
|
}) {
|
|
// 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()
|
|
const extraneous = new Set()
|
|
for (const filterNode of filterNodes) {
|
|
const { root } = filterNode
|
|
if (root !== ideal && root !== actual) {
|
|
throw new Error('invalid filterNode: outside idealTree/actualTree')
|
|
}
|
|
const rootTarget = root.target
|
|
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)
|
|
filterSet.add(edge.to.target)
|
|
}
|
|
filterSet.add(filterNode)
|
|
|
|
depth({
|
|
tree: filterNode,
|
|
visit: node => filterSet.add(node),
|
|
getChildren: node => {
|
|
node = node.target
|
|
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)
|
|
if (actualNode) {
|
|
for (const child of actualNode.children.values()) {
|
|
if (child.extraneous) {
|
|
extraneous.add(child)
|
|
}
|
|
}
|
|
}
|
|
|
|
return ideals.concat(actuals)
|
|
},
|
|
})
|
|
}
|
|
for (const extra of extraneous) {
|
|
filterSet.add(extra)
|
|
}
|
|
|
|
return depth({
|
|
tree: new Diff({ actual, ideal, filterSet, shrinkwrapInflated }),
|
|
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
|
|
}
|
|
|
|
// if the versions don't match, it's a change no matter what
|
|
if (ideal.version !== actual.version) {
|
|
return 'CHANGE'
|
|
}
|
|
|
|
const binsExist = ideal.binPaths.every((path) => existsSync(path))
|
|
|
|
// top nodes, links, and git deps won't have integrity, but do have resolved
|
|
// if neither node has integrity, the bins exist, and either (a) neither
|
|
// node has a resolved value or (b) they both do and match, then we can
|
|
// leave this one alone since we already know the versions match due to
|
|
// the condition above. The "neither has resolved" case (a) cannot be
|
|
// treated as a 'mark CHANGE and refetch', because shrinkwraps, bundles,
|
|
// and link deps may lack this information, and we don't want to try to
|
|
// go to the registry for something that isn't there.
|
|
const noIntegrity = !ideal.integrity && !actual.integrity
|
|
const noResolved = !ideal.resolved && !actual.resolved
|
|
const resolvedMatch = ideal.resolved && ideal.resolved === actual.resolved
|
|
if (noIntegrity && binsExist && (resolvedMatch || noResolved)) {
|
|
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.
|
|
const integrityMismatch = !ideal.integrity || !actual.integrity ||
|
|
!ssri.parse(ideal.integrity).match(actual.integrity)
|
|
if (integrityMismatch || !binsExist) {
|
|
return 'CHANGE'
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
const allChildren = node => {
|
|
if (!node) {
|
|
return new Map()
|
|
}
|
|
|
|
// if the node is root, and also a link, then what we really
|
|
// want is to traverse the target's children
|
|
if (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,
|
|
shrinkwrapInflated,
|
|
} = 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)
|
|
|
|
if (ideal && ideal.hasShrinkwrap && !shrinkwrapInflated.has(ideal)) {
|
|
// Guaranteed to get a diff.leaves here, because we always
|
|
// be called with a proper Diff object when ideal has a shrinkwrap
|
|
// that has not been inflated.
|
|
diff.leaves.push(diff)
|
|
return children
|
|
}
|
|
|
|
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,
|
|
shrinkwrapInflated,
|
|
})
|
|
}
|
|
|
|
if (diff.leaves && !children.length) {
|
|
diff.leaves.push(diff)
|
|
}
|
|
|
|
return children
|
|
}
|
|
|
|
const diffNode = ({
|
|
actual,
|
|
ideal,
|
|
children,
|
|
unchanged,
|
|
removed,
|
|
filterSet,
|
|
shrinkwrapInflated,
|
|
}) => {
|
|
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 || (!shrinkwrapInflated.has(ideal) && ideal.hasShrinkwrap)) {
|
|
if (action === 'REMOVE') {
|
|
removed.push(actual)
|
|
}
|
|
children.push(new Diff({ actual, ideal, filterSet, shrinkwrapInflated }))
|
|
} 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,
|
|
shrinkwrapInflated,
|
|
}))
|
|
}
|
|
}
|
|
|
|
// 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
|