1// An edge in the dependency graph
2// Represents a dependency relationship of some kind
3
4const util = require('util')
5const npa = require('npm-package-arg')
6const depValid = require('./dep-valid.js')
7
8class ArboristEdge {
9  constructor (edge) {
10    this.name = edge.name
11    this.spec = edge.spec
12    this.type = edge.type
13
14    const edgeFrom = edge.from?.location
15    const edgeTo = edge.to?.location
16    const override = edge.overrides?.value
17
18    if (edgeFrom != null) {
19      this.from = edgeFrom
20    }
21    if (edgeTo) {
22      this.to = edgeTo
23    }
24    if (edge.error) {
25      this.error = edge.error
26    }
27    if (edge.peerConflicted) {
28      this.peerConflicted = true
29    }
30    if (override) {
31      this.overridden = override
32    }
33  }
34}
35
36class Edge {
37  #accept
38  #error
39  #explanation
40  #from
41  #name
42  #spec
43  #to
44  #type
45
46  static types = Object.freeze([
47    'prod',
48    'dev',
49    'optional',
50    'peer',
51    'peerOptional',
52    'workspace',
53  ])
54
55  // XXX where is this used?
56  static errors = Object.freeze([
57    'DETACHED',
58    'MISSING',
59    'PEER LOCAL',
60    'INVALID',
61  ])
62
63  constructor (options) {
64    const { type, name, spec, accept, from, overrides } = options
65
66    // XXX are all of these error states even possible?
67    if (typeof spec !== 'string') {
68      throw new TypeError('must provide string spec')
69    }
70    if (!Edge.types.includes(type)) {
71      throw new TypeError(`invalid type: ${type}\n(valid types are: ${Edge.types.join(', ')})`)
72    }
73    if (type === 'workspace' && npa(spec).type !== 'directory') {
74      throw new TypeError('workspace edges must be a symlink')
75    }
76    if (typeof name !== 'string') {
77      throw new TypeError('must provide dependency name')
78    }
79    if (!from) {
80      throw new TypeError('must provide "from" node')
81    }
82    if (accept !== undefined) {
83      if (typeof accept !== 'string') {
84        throw new TypeError('accept field must be a string if provided')
85      }
86      this.#accept = accept || '*'
87    }
88    if (overrides !== undefined) {
89      this.overrides = overrides
90    }
91
92    this.#name = name
93    this.#type = type
94    this.#spec = spec
95    this.#explanation = null
96    this.#from = from
97
98    from.edgesOut.get(this.#name)?.detach()
99    from.addEdgeOut(this)
100
101    this.reload(true)
102    this.peerConflicted = false
103  }
104
105  satisfiedBy (node) {
106    if (node.name !== this.#name) {
107      return false
108    }
109
110    // NOTE: this condition means we explicitly do not support overriding
111    // bundled or shrinkwrapped dependencies
112    if (node.hasShrinkwrap || node.inShrinkwrap || node.inBundle) {
113      return depValid(node, this.rawSpec, this.#accept, this.#from)
114    }
115    return depValid(node, this.spec, this.#accept, this.#from)
116  }
117
118  // return the edge data, and an explanation of how that edge came to be here
119  explain (seen = []) {
120    if (!this.#explanation) {
121      const explanation = {
122        type: this.#type,
123        name: this.#name,
124        spec: this.spec,
125      }
126      if (this.rawSpec !== this.spec) {
127        explanation.rawSpec = this.rawSpec
128        explanation.overridden = true
129      }
130      if (this.bundled) {
131        explanation.bundled = this.bundled
132      }
133      if (this.error) {
134        explanation.error = this.error
135      }
136      if (this.#from) {
137        explanation.from = this.#from.explain(null, seen)
138      }
139      this.#explanation = explanation
140    }
141    return this.#explanation
142  }
143
144  get bundled () {
145    return !!this.#from?.package?.bundleDependencies?.includes(this.#name)
146  }
147
148  get workspace () {
149    return this.#type === 'workspace'
150  }
151
152  get prod () {
153    return this.#type === 'prod'
154  }
155
156  get dev () {
157    return this.#type === 'dev'
158  }
159
160  get optional () {
161    return this.#type === 'optional' || this.#type === 'peerOptional'
162  }
163
164  get peer () {
165    return this.#type === 'peer' || this.#type === 'peerOptional'
166  }
167
168  get type () {
169    return this.#type
170  }
171
172  get name () {
173    return this.#name
174  }
175
176  get rawSpec () {
177    return this.#spec
178  }
179
180  get spec () {
181    if (this.overrides?.value && this.overrides.value !== '*' && this.overrides.name === this.#name) {
182      if (this.overrides.value.startsWith('$')) {
183        const ref = this.overrides.value.slice(1)
184        // we may be a virtual root, if we are we want to resolve reference overrides
185        // from the real root, not the virtual one
186        const pkg = this.#from.sourceReference
187          ? this.#from.sourceReference.root.package
188          : this.#from.root.package
189        if (pkg.devDependencies?.[ref]) {
190          return pkg.devDependencies[ref]
191        }
192        if (pkg.optionalDependencies?.[ref]) {
193          return pkg.optionalDependencies[ref]
194        }
195        if (pkg.dependencies?.[ref]) {
196          return pkg.dependencies[ref]
197        }
198        if (pkg.peerDependencies?.[ref]) {
199          return pkg.peerDependencies[ref]
200        }
201
202        throw new Error(`Unable to resolve reference ${this.overrides.value}`)
203      }
204      return this.overrides.value
205    }
206    return this.#spec
207  }
208
209  get accept () {
210    return this.#accept
211  }
212
213  get valid () {
214    return !this.error
215  }
216
217  get missing () {
218    return this.error === 'MISSING'
219  }
220
221  get invalid () {
222    return this.error === 'INVALID'
223  }
224
225  get peerLocal () {
226    return this.error === 'PEER LOCAL'
227  }
228
229  get error () {
230    if (!this.#error) {
231      if (!this.#to) {
232        if (this.optional) {
233          this.#error = null
234        } else {
235          this.#error = 'MISSING'
236        }
237      } else if (this.peer && this.#from === this.#to.parent && !this.#from.isTop) {
238        this.#error = 'PEER LOCAL'
239      } else if (!this.satisfiedBy(this.#to)) {
240        this.#error = 'INVALID'
241      } else {
242        this.#error = 'OK'
243      }
244    }
245    if (this.#error === 'OK') {
246      return null
247    }
248    return this.#error
249  }
250
251  reload (hard = false) {
252    this.#explanation = null
253    if (this.#from.overrides) {
254      this.overrides = this.#from.overrides.getEdgeRule(this)
255    } else {
256      delete this.overrides
257    }
258    const newTo = this.#from.resolve(this.#name)
259    if (newTo !== this.#to) {
260      if (this.#to) {
261        this.#to.edgesIn.delete(this)
262      }
263      this.#to = newTo
264      this.#error = null
265      if (this.#to) {
266        this.#to.addEdgeIn(this)
267      }
268    } else if (hard) {
269      this.#error = null
270    }
271  }
272
273  detach () {
274    this.#explanation = null
275    if (this.#to) {
276      this.#to.edgesIn.delete(this)
277    }
278    this.#from.edgesOut.delete(this.#name)
279    this.#to = null
280    this.#error = 'DETACHED'
281    this.#from = null
282  }
283
284  get from () {
285    return this.#from
286  }
287
288  get to () {
289    return this.#to
290  }
291
292  toJSON () {
293    return new ArboristEdge(this)
294  }
295
296  [util.inspect.custom] () {
297    return this.toJSON()
298  }
299}
300
301module.exports = Edge
302