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