mirror of
https://github.com/nodejs/node.git
synced 2025-05-13 17:45:48 +00:00

PR-URL: https://github.com/nodejs/node/pull/44119 Reviewed-By: Ruy Adorno <ruyadorno@google.com> Reviewed-By: Mohammed Keyvanzadeh <mohammadkeyvanzade94@gmail.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: Myles Borins <myles.borins@gmail.com>
295 lines
6.8 KiB
JavaScript
295 lines
6.8 KiB
JavaScript
// 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
|