// An edge in the dependency graph // Represents a dependency relationship of some kind const util = require('util') const npa = require('npm-package-arg') const depValid = require('./dep-valid.js') const _from = Symbol('_from') const _to = Symbol('_to') const _type = Symbol('_type') const _spec = Symbol('_spec') const _accept = Symbol('_accept') const _name = Symbol('_name') const _error = Symbol('_error') const _loadError = Symbol('_loadError') const _setFrom = Symbol('_setFrom') const _explain = Symbol('_explain') const _explanation = Symbol('_explanation') const types = new Set([ 'prod', 'dev', 'optional', 'peer', 'peerOptional', 'workspace', ]) class ArboristEdge {} const printableEdge = (edge) => { const edgeFrom = edge.from && edge.from.location const edgeTo = edge.to && edge.to.location const override = edge.overrides && edge.overrides.value return Object.assign(new ArboristEdge(), { name: edge.name, spec: edge.spec, type: edge.type, ...(edgeFrom != null ? { from: edgeFrom } : {}), ...(edgeTo ? { to: edgeTo } : {}), ...(edge.error ? { error: edge.error } : {}), ...(edge.peerConflicted ? { peerConflicted: true } : {}), ...(override ? { overridden: override } : {}), }) } class Edge { constructor (options) { const { type, name, spec, accept, from, overrides } = options if (typeof spec !== 'string') { throw new TypeError('must provide string spec') } if (type === 'workspace' && npa(spec).type !== 'directory') { throw new TypeError('workspace edges must be a symlink') } this[_spec] = spec if (overrides !== undefined) { this.overrides = overrides } if (accept !== undefined) { if (typeof accept !== 'string') { throw new TypeError('accept field must be a string if provided') } this[_accept] = accept || '*' } if (typeof name !== 'string') { throw new TypeError('must provide dependency name') } this[_name] = name if (!types.has(type)) { throw new TypeError( `invalid type: ${type}\n` + `(valid types are: ${Edge.types.join(', ')})`) } this[_type] = type if (!from) { throw new TypeError('must provide "from" node') } this[_setFrom](from) this[_error] = this[_loadError]() this.peerConflicted = false } satisfiedBy (node) { if (node.name !== this.name) { return false } // NOTE: this condition means we explicitly do not support overriding // bundled or shrinkwrapped dependencies const spec = (node.hasShrinkwrap || node.inShrinkwrap || node.inBundle) ? this.rawSpec : this.spec return depValid(node, spec, this.accept, this.from) } explain (seen = []) { if (this[_explanation]) { return this[_explanation] } return this[_explanation] = this[_explain](seen) } // return the edge data, and an explanation of how that edge came to be here [_explain] (seen) { const { error, from, bundled } = this return { type: this.type, name: this.name, spec: this.spec, ...(this.rawSpec !== this.spec ? { rawSpec: this.rawSpec, overridden: true, } : {}), ...(bundled ? { bundled } : {}), ...(error ? { error } : {}), ...(from ? { from: from.explain(null, seen) } : {}), } } get bundled () { if (!this.from) { return false } const { package: { bundleDependencies = [] } } = this.from return bundleDependencies.includes(this.name) } get workspace () { return this[_type] === 'workspace' } get prod () { return this[_type] === 'prod' } get dev () { return this[_type] === 'dev' } get optional () { return this[_type] === 'optional' || this[_type] === 'peerOptional' } get peer () { return this[_type] === 'peer' || this[_type] === 'peerOptional' } get type () { return this[_type] } get name () { return this[_name] } get rawSpec () { return this[_spec] } get spec () { if (this.overrides && this.overrides.value && this.overrides.name === this.name) { if (this.overrides.value.startsWith('$')) { const ref = this.overrides.value.slice(1) // we may be a virtual root, if we are we want to resolve reference overrides // from the real root, not the virtual one const pkg = this.from.sourceReference ? this.from.sourceReference.root.package : this.from.root.package const overrideSpec = (pkg.devDependencies && pkg.devDependencies[ref]) || (pkg.optionalDependencies && pkg.optionalDependencies[ref]) || (pkg.dependencies && pkg.dependencies[ref]) || (pkg.peerDependencies && pkg.peerDependencies[ref]) if (overrideSpec) { return overrideSpec } throw new Error(`Unable to resolve reference ${this.overrides.value}`) } return this.overrides.value } return this[_spec] } get accept () { return this[_accept] } get valid () { return !this.error } get missing () { return this.error === 'MISSING' } get invalid () { return this.error === 'INVALID' } get peerLocal () { return this.error === 'PEER LOCAL' } get error () { this[_error] = this[_error] || this[_loadError]() return this[_error] === 'OK' ? null : this[_error] } [_loadError] () { return !this[_to] ? (this.optional ? null : 'MISSING') : this.peer && this.from === this.to.parent && !this.from.isTop ? 'PEER LOCAL' : !this.satisfiedBy(this.to) ? 'INVALID' : 'OK' } reload (hard = false) { this[_explanation] = null if (this[_from].overrides) { this.overrides = this[_from].overrides.getEdgeRule(this) } else { delete this.overrides } const newTo = this[_from].resolve(this.name) if (newTo !== this[_to]) { if (this[_to]) { this[_to].edgesIn.delete(this) } this[_to] = newTo this[_error] = this[_loadError]() if (this[_to]) { this[_to].addEdgeIn(this) } } else if (hard) { this[_error] = this[_loadError]() } } detach () { this[_explanation] = null if (this[_to]) { this[_to].edgesIn.delete(this) } this[_from].edgesOut.delete(this.name) this[_to] = null this[_error] = 'DETACHED' this[_from] = null } [_setFrom] (node) { this[_explanation] = null this[_from] = node if (node.edgesOut.has(this.name)) { node.edgesOut.get(this.name).detach() } node.addEdgeOut(this) this.reload() } get from () { return this[_from] } get to () { return this[_to] } toJSON () { return printableEdge(this) } [util.inspect.custom] () { return this.toJSON() } } Edge.types = [...types] Edge.errors = [ 'DETACHED', 'MISSING', 'PEER LOCAL', 'INVALID', ] module.exports = Edge