Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
diff.js331 linesDownload Raw Back to lib
1// a tree representing the difference between two trees
2// A Diff node's parent is not necessarily the parent of
3// the node location it refers to, but rather the highest level
4// node that needs to be either changed or removed.
5// Thus, the root Diff node is the shallowest change required
6// for a given branch of the tree being mutated.
7
8const { depth } = require('treeverse')
9const { existsSync } = require('node:fs')
10
11const ssri = require('ssri')
12
13class Diff {
14  constructor ({ actual, ideal, filterSet, shrinkwrapInflated, omit }) {
15    this.omit = omit
16    this.filterSet = filterSet
17    this.shrinkwrapInflated = shrinkwrapInflated
18    this.children = []
19    this.actual = actual
20    this.ideal = ideal
21    if (this.ideal) {
22      this.resolved = this.ideal.resolved
23      this.integrity = this.ideal.integrity
24    }
25    this.action = getAction(this)
26    this.parent = null
27    // the set of leaf nodes that we rake up to the top level
28    this.leaves = []
29    // the set of nodes that don't change in this branch of the tree
30    this.unchanged = []
31    // the set of nodes that will be removed in this branch of the tree
32    this.removed = []
33  }
34
35  static calculate ({
36    actual,
37    ideal,
38    filterNodes = [],
39    shrinkwrapInflated = new Set(),
40    omit = new Set(),
41  }) {
42    // if there's a filterNode, then:
43    // - get the path from the root to the filterNode.  The root or
44    //   root.target should have an edge either to the filterNode or
45    //   a link to the filterNode.  If not, abort.  Add the path to the
46    //   filterSet.
47    // - Add set of Nodes depended on by the filterNode to filterSet.
48    // - Anything outside of that set should be ignored by getChildren
49    const filterSet = new Set()
50    const extraneous = new Set()
51    for (const filterNode of filterNodes) {
52      const { root } = filterNode
53      if (root !== ideal && root !== actual) {
54        throw new Error('invalid filterNode: outside idealTree/actualTree')
55      }
56      const rootTarget = root.target
57      const edge = [...rootTarget.edgesOut.values()].filter(e => {
58        return e.to && (e.to === filterNode || e.to.target === filterNode)
59      })[0]
60      filterSet.add(root)
61      filterSet.add(rootTarget)
62      filterSet.add(ideal)
63      filterSet.add(actual)
64      if (edge && edge.to) {
65        filterSet.add(edge.to)
66        filterSet.add(edge.to.target)
67      }
68      filterSet.add(filterNode)
69
70      depth({
71        tree: filterNode,
72        visit: node => filterSet.add(node),
73        getChildren: node => {
74          const orig = node
75          node = node.target
76          const loc = node.location
77          const idealNode = ideal.inventory.get(loc)
78          const ideals = !idealNode ? []
79            : [...idealNode.edgesOut.values()].filter(e => e.to).map(e => e.to)
80          const actualNode = actual.inventory.get(loc)
81          const actuals = !actualNode ? []
82            : [...actualNode.edgesOut.values()].filter(e => e.to).map(e => e.to)
83          if (actualNode) {
84            for (const child of actualNode.children.values()) {
85              if (child.extraneous) {
86                extraneous.add(child)
87              }
88            }
89          }
90
91          const result = ideals.concat(actuals)
92          // Include link targets so store entries end up in filterSet
93          if (orig.isLink) {
94            result.push(node)
95          }
96          return result
97        },
98      })
99    }
100    for (const extra of extraneous) {
101      filterSet.add(extra)
102    }
103
104    return depth({
105      tree: new Diff({ actual, ideal, filterSet, shrinkwrapInflated, omit }),
106      getChildren,
107      leave,
108    })
109  }
110}
111
112const getAction = ({ actual, ideal }) => {
113  if (!ideal) {
114    return 'REMOVE'
115  }
116
117  // bundled meta-deps are copied over to the ideal tree when we visit it,
118  // so they'll appear to be missing here.  There's no need to handle them
119  // in the diff, though, because they'll be replaced at reify time anyway
120  // Otherwise, add the missing node.
121  if (!actual) {
122    return ideal.inDepBundle ? null : 'ADD'
123  }
124
125  // always ignore the root node
126  if (ideal.isRoot && actual.isRoot) {
127    return null
128  }
129
130  // if the versions don't match, it's a change no matter what
131  if (ideal.version !== actual.version) {
132    return 'CHANGE'
133  }
134
135  const binsExist = ideal.binPaths.every((path) => existsSync(path))
136
137  // top nodes, links, and git deps won't have integrity, but do have resolved
138  // if neither node has integrity, the bins exist, and either (a) neither
139  // node has a resolved value or (b) they both do and match, then we can
140  // leave this one alone since we already know the versions match due to
141  // the condition above.  The "neither has resolved" case (a) cannot be
142  // treated as a 'mark CHANGE and refetch', because shrinkwraps, bundles,
143  // and link deps may lack this information, and we don't want to try to
144  // go to the registry for something that isn't there.
145  const noIntegrity = !ideal.integrity && !actual.integrity
146  const noResolved = !ideal.resolved && !actual.resolved
147  const resolvedMatch = ideal.resolved && ideal.resolved === actual.resolved
148  if (noIntegrity && binsExist && (resolvedMatch || noResolved)) {
149    return null
150  }
151
152  // otherwise, verify that it's the same bits
153  // note that if ideal has integrity, and resolved doesn't, we treat
154  // that as a 'change', so that it gets re-fetched and locked down.
155  const integrityMismatch = !ideal.integrity || !actual.integrity ||
156    !ssri.parse(ideal.integrity).match(actual.integrity)
157  if (integrityMismatch || !binsExist) {
158    return 'CHANGE'
159  }
160
161  return null
162}
163
164const allChildren = node => {
165  if (!node) {
166    return new Map()
167  }
168
169  // if the node is root, and also a link, then what we really
170  // want is to traverse the target's children
171  if (node.isRoot && node.isLink) {
172    return allChildren(node.target)
173  }
174
175  const kids = new Map()
176  for (const n of [node, ...node.fsChildren]) {
177    for (const kid of n.children.values()) {
178      kids.set(kid.path, kid)
179    }
180  }
181  return kids
182}
183
184// functions for the walk options when we traverse the trees
185// to create the diff tree
186const getChildren = diff => {
187  const children = []
188  const {
189    actual,
190    ideal,
191    unchanged,
192    removed,
193    filterSet,
194    shrinkwrapInflated,
195    omit,
196  } = diff
197
198  // Note: we DON'T diff fsChildren themselves, because they are either
199  // included in the package contents, or part of some other project, and
200  // will never appear in legacy shrinkwraps anyway.  but we _do_ include the
201  // child nodes of fsChildren, because those are nodes that we are typically
202  // responsible for installing.
203  const actualKids = allChildren(actual)
204  const idealKids = allChildren(ideal)
205
206  if (ideal && ideal.hasShrinkwrap && !shrinkwrapInflated.has(ideal)) {
207    // Guaranteed to get a diff.leaves here, because we always
208    // be called with a proper Diff object when ideal has a shrinkwrap
209    // that has not been inflated.
210    diff.leaves.push(diff)
211    return children
212  }
213
214  const paths = new Set([...actualKids.keys(), ...idealKids.keys()])
215  for (const path of paths) {
216    const actual = actualKids.get(path)
217    const ideal = idealKids.get(path)
218    diffNode({
219      actual,
220      ideal,
221      children,
222      unchanged,
223      removed,
224      filterSet,
225      shrinkwrapInflated,
226      omit,
227    })
228  }
229
230  if (diff.leaves && !children.length) {
231    diff.leaves.push(diff)
232  }
233
234  return children
235}
236
237const diffNode = ({
238  actual,
239  ideal,
240  children,
241  unchanged,
242  removed,
243  filterSet,
244  shrinkwrapInflated,
245  omit,
246}) => {
247  if (filterSet.size && !(filterSet.has(ideal) || filterSet.has(actual))) {
248    return
249  }
250
251  if (ideal?.shouldOmit?.(omit)) {
252    ideal.inert = true
253  }
254
255  // Treat inert nodes as undefined for the purposes of diffing.
256  if (ideal?.inert) {
257    ideal = undefined
258  }
259  if (!actual && !ideal) {
260    return
261  }
262
263  const action = getAction({ actual, ideal })
264
265  // if it's a match, then get its children
266  // otherwise, this is the child diff node
267  if (action || (!shrinkwrapInflated.has(ideal) && ideal.hasShrinkwrap)) {
268    if (action === 'REMOVE') {
269      removed.push(actual)
270    }
271    children.push(new Diff({ actual, ideal, filterSet, shrinkwrapInflated, omit }))
272  } else {
273    unchanged.push(ideal)
274    // !*! Weird dirty hack warning !*!
275    //
276    // Bundled deps aren't loaded in the ideal tree, because we don't know
277    // what they are going to be without unpacking.  Swap them over now if
278    // the bundling node isn't changing, so we don't prune them later.
279    //
280    // It's a little bit dirty to be doing this here, since it means that
281    // diffing trees can mutate them, but otherwise we have to walk over
282    // all unchanging bundlers and correct the diff later, so it's more
283    // efficient to just fix it while we're passing through already.
284    //
285    // Note that moving over a bundled dep will break the links to other
286    // deps under this parent, which may have been transitively bundled.
287    // Breaking those links means that we'll no longer see the transitive
288    // dependency, meaning that it won't appear as bundled any longer!
289    // In order to not end up dropping transitively bundled deps, we have
290    // to get the list of nodes to move, then move them all at once, rather
291    // than moving them one at a time in the first loop.
292    const bd = ideal.package.bundleDependencies
293    if (actual && bd && bd.length) {
294      const bundledChildren = []
295      for (const node of actual.children.values()) {
296        if (node.inBundle) {
297          bundledChildren.push(node)
298        }
299      }
300      for (const node of bundledChildren) {
301        node.parent = ideal
302      }
303    }
304    children.push(...getChildren({
305      actual,
306      ideal,
307      unchanged,
308      removed,
309      filterSet,
310      shrinkwrapInflated,
311      omit,
312    }))
313  }
314}
315
316// set the parentage in the leave step so that we aren't attaching
317// child nodes only to remove them later.  also bubble up the unchanged
318// nodes so that we can move them out of staging in the reification step.
319const leave = (diff, children) => {
320  children.forEach(kid => {
321    kid.parent = diff
322    diff.leaves.push(...kid.leaves)
323    diff.unchanged.push(...kid.unchanged)
324    diff.removed.push(...kid.removed)
325  })
326  diff.children = children
327  return diff
328}
329
330module.exports = Diff
331 
codekingpro/portable-devtools · Team Ai