Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
edge.js365 linesDownload Raw Back to lib
1// An edge in the dependency graph
2// Represents a dependency relationship of some kind
3
4const util = require('node:util')
5const npa = require('npm-package-arg')
6const depValid = require('./dep-valid.js')
7const OverrideSet = require('./override-set.js')
8
9class ArboristEdge {
10  constructor (edge) {
11    this.name = edge.name
12    this.spec = edge.spec
13    this.type = edge.type
14
15    const edgeFrom = edge.from?.location
16    const edgeTo = edge.to?.location
17    const override = edge.overrides?.value
18
19    if (edgeFrom != null) {
20      this.from = edgeFrom
21    }
22    if (edgeTo) {
23      this.to = edgeTo
24    }
25    if (edge.error) {
26      this.error = edge.error
27    }
28    if (edge.peerConflicted) {
29      this.peerConflicted = true
30    }
31    if (override) {
32      this.overridden = override
33    }
34  }
35}
36
37class Edge {
38  #accept
39  #error
40  #explanation
41  #from
42  #name
43  #spec
44  #to
45  #type
46
47  static types = Object.freeze([
48    'prod',
49    'dev',
50    'optional',
51    'peer',
52    'peerOptional',
53    'workspace',
54  ])
55
56  // XXX where is this used?
57  static errors = Object.freeze([
58    'DETACHED',
59    'MISSING',
60    'PEER LOCAL',
61    'INVALID',
62  ])
63
64  constructor (options) {
65    const { type, name, spec, accept, from, overrides } = options
66
67    // XXX are all of these error states even possible?
68    if (typeof spec !== 'string') {
69      throw new TypeError('must provide string spec')
70    }
71    if (!Edge.types.includes(type)) {
72      throw new TypeError(`invalid type: ${type}\n(valid types are: ${Edge.types.join(', ')})`)
73    }
74    if (type === 'workspace' && npa(spec).type !== 'directory') {
75      throw new TypeError('workspace edges must be a symlink')
76    }
77    if (typeof name !== 'string') {
78      throw new TypeError('must provide dependency name')
79    }
80    if (!from) {
81      throw new TypeError('must provide "from" node')
82    }
83    if (accept !== undefined) {
84      if (typeof accept !== 'string') {
85        throw new TypeError('accept field must be a string if provided')
86      }
87      this.#accept = accept || '*'
88    }
89    if (overrides !== undefined) {
90      this.overrides = overrides
91    }
92
93    this.#name = name
94    this.#type = type
95    this.#spec = spec
96    this.#explanation = null
97    this.#from = from
98
99    from.edgesOut.get(this.#name)?.detach()
100    from.addEdgeOut(this)
101
102    this.reload(true)
103    this.peerConflicted = false
104  }
105
106  satisfiedBy (node) {
107    if (node.name !== this.#name || !this.#from) {
108      return false
109    }
110
111    // NOTE: this condition means we explicitly do not support overriding
112    // bundled or shrinkwrapped dependencies
113    if (node.hasShrinkwrap || node.inShrinkwrap || node.inBundle) {
114      return depValid(node, this.rawSpec, this.#accept, this.#from)
115    }
116
117    // If there's no override we just use the spec.
118    if (!this.overrides?.keySpec) {
119      return depValid(node, this.spec, this.#accept, this.#from)
120    }
121    // There's some override. If the target node satisfies the overriding spec
122    // then it's okay.
123    if (depValid(node, this.spec, this.#accept, this.#from)) {
124      return true
125    }
126    // If it doesn't, then it should at least satisfy the original spec.
127    if (!depValid(node, this.rawSpec, this.#accept, this.#from)) {
128      return false
129    }
130    // It satisfies the original spec, not the overriding spec. We need to make
131    // sure it doesn't use the overridden spec.
132    // For example:
133    //   we might have an ^8.0.0 rawSpec, and an override that makes
134    //   keySpec=8.23.0 and the override value spec=9.0.0.
135    //   If the node is 9.0.0, then it's okay because it's consistent with spec.
136    //   If the node is 8.24.0, then it's okay because it's consistent with the rawSpec.
137    //   If the node is 8.23.0, then it's not okay because even though it's consistent
138    //   with the rawSpec, it's also consistent with the keySpec.
139    //   So we're looking for ^8.0.0 or 9.0.0 and not 8.23.0.
140    return !depValid(node, this.overrides.keySpec, this.#accept, this.#from)
141  }
142
143  // return the edge data, and an explanation of how that edge came to be here
144  explain (seen = []) {
145    if (!this.#explanation) {
146      const explanation = {
147        type: this.#type,
148        name: this.#name,
149        spec: this.spec,
150      }
151      if (this.rawSpec !== this.spec) {
152        explanation.rawSpec = this.rawSpec
153        explanation.overridden = true
154      }
155      if (this.bundled) {
156        explanation.bundled = this.bundled
157      }
158      if (this.error) {
159        explanation.error = this.error
160      }
161      if (this.#from) {
162        explanation.from = this.#from.explain(null, seen)
163      }
164      this.#explanation = explanation
165    }
166    return this.#explanation
167  }
168
169  get bundled () {
170    return !!this.#from?.package?.bundleDependencies?.includes(this.#name)
171  }
172
173  get workspace () {
174    return this.#type === 'workspace'
175  }
176
177  get prod () {
178    return this.#type === 'prod'
179  }
180
181  get dev () {
182    return this.#type === 'dev'
183  }
184
185  get optional () {
186    return this.#type === 'optional' || this.#type === 'peerOptional'
187  }
188
189  get peer () {
190    return this.#type === 'peer' || this.#type === 'peerOptional'
191  }
192
193  get type () {
194    return this.#type
195  }
196
197  get name () {
198    return this.#name
199  }
200
201  get rawSpec () {
202    return this.#spec
203  }
204
205  get spec () {
206    if (this.overrides?.value && this.overrides.value !== '*' && this.overrides.name === this.#name) {
207      if (this.overrides.value.startsWith('$')) {
208        const ref = this.overrides.value.slice(1)
209        let pkg = this.#from?.sourceReference
210          ? this.#from?.sourceReference.root.package
211          : this.#from?.root?.package
212
213        let specValue = this.#calculateReferentialOverrideSpec(ref, pkg)
214
215        // If the package isn't found in the root package, fall back to the local package.
216        if (!specValue) {
217          pkg = this.#from?.package
218          specValue = this.#calculateReferentialOverrideSpec(ref, pkg)
219        }
220
221        if (specValue) {
222          return specValue
223        }
224        throw new Error(`Unable to resolve reference ${this.overrides.value}`)
225      }
226      return this.overrides.value
227    }
228    return this.#spec
229  }
230
231  #calculateReferentialOverrideSpec (ref, pkg) {
232    if (pkg.devDependencies?.[ref]) {
233      return pkg.devDependencies[ref]
234    }
235    if (pkg.optionalDependencies?.[ref]) {
236      return pkg.optionalDependencies[ref]
237    }
238    if (pkg.dependencies?.[ref]) {
239      return pkg.dependencies[ref]
240    }
241    if (pkg.peerDependencies?.[ref]) {
242      return pkg.peerDependencies[ref]
243    }
244  }
245
246  get accept () {
247    return this.#accept
248  }
249
250  get valid () {
251    return !this.error
252  }
253
254  get missing () {
255    return this.error === 'MISSING'
256  }
257
258  get invalid () {
259    return this.error === 'INVALID'
260  }
261
262  get peerLocal () {
263    return this.error === 'PEER LOCAL'
264  }
265
266  get error () {
267    if (!this.#error) {
268      if (!this.#to) {
269        if (this.optional) {
270          this.#error = null
271        } else {
272          this.#error = 'MISSING'
273        }
274      } else if (this.peer && this.#from === this.#to.parent && !this.#from?.isTop) {
275        this.#error = 'PEER LOCAL'
276      } else if (!this.satisfiedBy(this.#to)) {
277        this.#error = 'INVALID'
278      } else if (this.overrides && this.#to.overrides && this.#to.edgesOut.size && OverrideSet.doOverrideSetsConflict(this.overrides, this.#to.overrides)) {
279        // Check for conflicts between the edge's override set and the target node's override set.
280        // This catches cases where different parts of the tree have genuinely incompatible
281        // version requirements for the same package.
282        // The improved conflict detection uses semantic comparison (checking for incompatible
283        // version ranges) rather than pure structural equality, avoiding false positives from:
284        // - Reference overrides ($syntax) that resolve to compatible versions
285        // - Peer dependencies with different but compatible override contexts
286        // Note: We only check if the target has dependencies (edgesOut.size > 0), since
287        // override conflicts are only relevant if the target has its own dependencies.
288        this.#error = 'INVALID'
289      } else {
290        this.#error = 'OK'
291      }
292    }
293    if (this.#error === 'OK') {
294      return null
295    }
296    return this.#error
297  }
298
299  reload (hard = false) {
300    this.#explanation = null
301
302    let needToUpdateOverrideSet = false
303    let newOverrideSet
304    let oldOverrideSet
305    if (this.#from?.overrides) {
306      newOverrideSet = this.#from.overrides.getEdgeRule(this)
307      if (newOverrideSet && !newOverrideSet.isEqual(this.overrides)) {
308        // If there's a new different override set we need to propagate it to the nodes.
309        // If we're deleting the override set then there's no point propagating it right now since it will be filled with another value later.
310        needToUpdateOverrideSet = true
311        oldOverrideSet = this.overrides
312        this.overrides = newOverrideSet
313      }
314    } else {
315      delete this.overrides
316    }
317    const newTo = this.#from?.resolve(this.#name)
318    if (newTo !== this.#to) {
319      if (this.#to) {
320        this.#to.deleteEdgeIn(this)
321      }
322      this.#to = newTo
323      this.#error = null
324      if (this.#to) {
325        this.#to.addEdgeIn(this)
326      }
327    } else if (hard) {
328      this.#error = null
329    } else if (needToUpdateOverrideSet && this.#to) {
330      // Propagate the new override set to the target node.
331      this.#to.updateOverridesEdgeInRemoved(oldOverrideSet)
332      this.#to.updateOverridesEdgeInAdded(newOverrideSet)
333    }
334  }
335
336  detach () {
337    this.#explanation = null
338    if (this.#to) {
339      this.#to.deleteEdgeIn(this)
340    }
341    this.#from?.edgesOut.delete(this.#name)
342    this.#to = null
343    this.#error = 'DETACHED'
344    this.#from = null
345  }
346
347  get from () {
348    return this.#from
349  }
350
351  get to () {
352    return this.#to
353  }
354
355  toJSON () {
356    return new ArboristEdge(this)
357  }
358
359  [util.inspect.custom] () {
360    return this.toJSON()
361  }
362}
363
364module.exports = Edge
365 
codekingpro/portable-devtools · Team Ai