@npmcli/arborist
Advanced tools
Comparing version 5.6.1 to 6.0.0-pre.0
@@ -18,30 +18,26 @@ // mix-in implementing the loadActual method | ||
const _loadFSNode = Symbol('loadFSNode') | ||
const _newNode = Symbol('newNode') | ||
const _newLink = Symbol('newLink') | ||
const _loadFSTree = Symbol('loadFSTree') | ||
const _loadFSChildren = Symbol('loadFSChildren') | ||
const _findMissingEdges = Symbol('findMissingEdges') | ||
const _findFSParents = Symbol('findFSParents') | ||
const _resetDepFlags = Symbol('resetDepFlags') | ||
// public symbols | ||
const _changePath = Symbol.for('_changePath') | ||
const _global = Symbol.for('global') | ||
const _loadWorkspaces = Symbol.for('loadWorkspaces') | ||
const _rpcache = Symbol.for('realpathCache') | ||
const _stcache = Symbol.for('statCache') | ||
// private symbols | ||
const _actualTree = Symbol('actualTree') | ||
const _actualTreeLoaded = Symbol('actualTreeLoaded') | ||
const _rpcache = Symbol.for('realpathCache') | ||
const _stcache = Symbol.for('statCache') | ||
const _topNodes = Symbol('linkTargets') | ||
const _actualTreePromise = Symbol('actualTreePromise') | ||
const _cache = Symbol('nodeLoadingCache') | ||
const _filter = Symbol('filter') | ||
const _findMissingEdges = Symbol('findMissingEdges') | ||
const _loadActual = Symbol('loadActual') | ||
const _loadActualVirtually = Symbol('loadActualVirtually') | ||
const _loadActualActually = Symbol('loadActualActually') | ||
const _loadWorkspaces = Symbol.for('loadWorkspaces') | ||
const _loadWorkspaceTargets = Symbol('loadWorkspaceTargets') | ||
const _actualTreePromise = Symbol('actualTreePromise') | ||
const _actualTree = Symbol('actualTree') | ||
const _loadFSChildren = Symbol('loadFSChildren') | ||
const _loadFSNode = Symbol('loadFSNode') | ||
const _loadFSTree = Symbol('loadFSTree') | ||
const _newLink = Symbol('newLink') | ||
const _newNode = Symbol('newNode') | ||
const _topNodes = Symbol('linkTargets') | ||
const _transplant = Symbol('transplant') | ||
const _transplantFilter = Symbol('transplantFilter') | ||
const _filter = Symbol('filter') | ||
const _global = Symbol.for('global') | ||
const _changePath = Symbol.for('_changePath') | ||
module.exports = cls => class ActualLoader extends cls { | ||
@@ -79,33 +75,40 @@ constructor (options) { | ||
[_resetDepFlags] (tree, root) { | ||
// reset all deps to extraneous prior to recalc | ||
if (!root) { | ||
for (const node of tree.inventory.values()) { | ||
node.extraneous = true | ||
} | ||
// public method | ||
async loadActual (options = {}) { | ||
// In the past this.actualTree was set as a promise that eventually | ||
// resolved, and overwrite this.actualTree with the resolved value. This | ||
// was a problem because virtually no other code expects this.actualTree to | ||
// be a promise. Instead we only set it once resolved, and also return it | ||
// from the promise so that it is what's returned from this function when | ||
// awaited. | ||
if (this.actualTree) { | ||
return this.actualTree | ||
} | ||
if (!this[_actualTreePromise]) { | ||
// allow the user to set options on the ctor as well. | ||
// XXX: deprecate separate method options objects. | ||
options = { ...this.options, ...options } | ||
// only reset root flags if we're not re-rooting, | ||
// otherwise leave as-is | ||
calcDepFlags(tree, !root) | ||
return tree | ||
} | ||
this[_actualTreePromise] = this[_loadActual](options) | ||
.then(tree => { | ||
// reset all deps to extraneous prior to recalc | ||
if (!options.root) { | ||
for (const node of tree.inventory.values()) { | ||
node.extraneous = true | ||
} | ||
} | ||
// public method | ||
async loadActual (options = {}) { | ||
// allow the user to set options on the ctor as well. | ||
// XXX: deprecate separate method options objects. | ||
options = { ...this.options, ...options } | ||
// stash the promise so that we don't ever have more than one | ||
// going at the same time. This is so that buildIdealTree can | ||
// default to the actualTree if no shrinkwrap present, but | ||
// reify() can still call buildIdealTree and loadActual in parallel | ||
// safely. | ||
return this.actualTree ? this.actualTree | ||
: this[_actualTreePromise] ? this[_actualTreePromise] | ||
: this[_actualTreePromise] = this[_loadActual](options) | ||
.then(tree => this[_resetDepFlags](tree, options.root)) | ||
.then(tree => this.actualTree = treeCheck(tree)) | ||
// only reset root flags if we're not re-rooting, | ||
// otherwise leave as-is | ||
calcDepFlags(tree, !options.root) | ||
this.actualTree = treeCheck(tree) | ||
return this.actualTree | ||
}) | ||
} | ||
return this[_actualTreePromise] | ||
} | ||
// return the promise so that we don't ever have more than one going at the | ||
// same time. This is so that buildIdealTree can default to the actualTree | ||
// if no shrinkwrap present, but reify() can still call buildIdealTree and | ||
// loadActual in parallel safely. | ||
@@ -127,4 +130,3 @@ async [_loadActual] (options) { | ||
const real = await realpath(this.path, this[_rpcache], this[_stcache]) | ||
const newNodeOrLink = this.path === real ? _newNode : _newLink | ||
this[_actualTree] = await this[newNodeOrLink]({ | ||
const params = { | ||
path: this.path, | ||
@@ -135,64 +137,92 @@ realpath: real, | ||
loadOverrides: true, | ||
} | ||
if (this.path === real) { | ||
this[_actualTree] = this[_newNode](params) | ||
} else { | ||
this[_actualTree] = await this[_newLink](params) | ||
} | ||
} else { | ||
// not in global mode, hidden lockfile is allowed, load root pkg too | ||
this[_actualTree] = await this[_loadFSNode]({ | ||
path: this.path, | ||
real: await realpath(this.path, this[_rpcache], this[_stcache]), | ||
loadOverrides: true, | ||
}) | ||
return this[_loadActualActually]({ root, ignoreMissing, global }) | ||
} | ||
// not in global mode, hidden lockfile is allowed, load root pkg too | ||
this[_actualTree] = await this[_loadFSNode]({ | ||
path: this.path, | ||
real: await realpath(this.path, this[_rpcache], this[_stcache]), | ||
loadOverrides: true, | ||
}) | ||
this[_actualTree].assertRootOverrides() | ||
this[_actualTree].assertRootOverrides() | ||
// if forceActual is set, don't even try the hidden lockfile | ||
if (!forceActual) { | ||
// Note: hidden lockfile will be rejected if it's not the latest thing | ||
// in the folder, or if any of the entries in the hidden lockfile are | ||
// missing. | ||
const meta = await Shrinkwrap.load({ | ||
path: this[_actualTree].path, | ||
hiddenLockfile: true, | ||
resolveOptions: this.options, | ||
}) | ||
// if forceActual is set, don't even try the hidden lockfile | ||
if (!forceActual) { | ||
// Note: hidden lockfile will be rejected if it's not the latest thing | ||
// in the folder, or if any of the entries in the hidden lockfile are | ||
// missing. | ||
if (meta.loadedFromDisk) { | ||
this[_actualTree].meta = meta | ||
// have to load on a new Arborist object, so we don't assign | ||
// the virtualTree on this one! Also, the weird reference is because | ||
// we can't easily get a ref to Arborist in this module, without | ||
// creating a circular reference, since this class is a mixin used | ||
// to build up the Arborist class itself. | ||
await new this.constructor({ ...this.options }).loadVirtual({ | ||
root: this[_actualTree], | ||
}) | ||
await this[_loadWorkspaces](this[_actualTree]) | ||
this[_transplant](root) | ||
return this[_actualTree] | ||
} | ||
} | ||
const meta = await Shrinkwrap.load({ | ||
path: this[_actualTree].path, | ||
hiddenLockfile: true, | ||
lockfileVersion: this.options.lockfileVersion, | ||
resolveOptions: this.options, | ||
}) | ||
if (meta.loadedFromDisk) { | ||
this[_actualTree].meta = meta | ||
return this[_loadActualVirtually]({ root }) | ||
} | ||
this[_actualTree].meta = meta | ||
} | ||
const meta = await Shrinkwrap.load({ | ||
path: this[_actualTree].path, | ||
lockfileVersion: this.options.lockfileVersion, | ||
resolveOptions: this.options, | ||
}) | ||
this[_actualTree].meta = meta | ||
return this[_loadActualActually]({ root, ignoreMissing }) | ||
} | ||
async [_loadActualVirtually] ({ root }) { | ||
// have to load on a new Arborist object, so we don't assign | ||
// the virtualTree on this one! Also, the weird reference is because | ||
// we can't easily get a ref to Arborist in this module, without | ||
// creating a circular reference, since this class is a mixin used | ||
// to build up the Arborist class itself. | ||
await new this.constructor({ ...this.options }).loadVirtual({ | ||
root: this[_actualTree], | ||
}) | ||
await this[_loadFSTree](this[_actualTree]) | ||
await this[_loadWorkspaces](this[_actualTree]) | ||
this[_transplant](root) | ||
return this[_actualTree] | ||
} | ||
// if there are workspace targets without Link nodes created, load | ||
// the targets, so that we know what they are. | ||
if (this[_actualTree].workspaces && this[_actualTree].workspaces.size) { | ||
const promises = [] | ||
for (const path of this[_actualTree].workspaces.values()) { | ||
if (!this[_cache].has(path)) { | ||
// workspace overrides use the root overrides | ||
const p = this[_loadFSNode]({ path, root: this[_actualTree], useRootOverrides: true }) | ||
.then(node => this[_loadFSTree](node)) | ||
promises.push(p) | ||
} | ||
} | ||
await Promise.all(promises) | ||
} | ||
async [_loadActualActually] ({ root, ignoreMissing, global }) { | ||
await this[_loadFSTree](this[_actualTree]) | ||
await this[_loadWorkspaces](this[_actualTree]) | ||
await this[_loadWorkspaceTargets](this[_actualTree]) | ||
if (!ignoreMissing) { | ||
await this[_findMissingEdges]() | ||
} | ||
this[_findFSParents]() | ||
// try to find a node that is the parent in a fs tree sense, but not a | ||
// node_modules tree sense, of any link targets. this allows us to | ||
// resolve deps that node will find, but a legacy npm view of the | ||
// world would not have noticed. | ||
for (const path of this[_topNodes]) { | ||
const node = this[_cache].get(path) | ||
if (node && !node.parent && !node.fsParent) { | ||
for (const p of walkUp(dirname(path))) { | ||
if (this[_cache].has(p)) { | ||
node.fsParent = this[_cache].get(p) | ||
break | ||
} | ||
} | ||
} | ||
} | ||
this[_transplant](root) | ||
@@ -216,21 +246,2 @@ | ||
// if there are workspace targets without Link nodes created, load | ||
// the targets, so that we know what they are. | ||
async [_loadWorkspaceTargets] (tree) { | ||
if (!tree.workspaces || !tree.workspaces.size) { | ||
return | ||
} | ||
const promises = [] | ||
for (const path of tree.workspaces.values()) { | ||
if (!this[_cache].has(path)) { | ||
// workspace overrides use the root overrides | ||
const p = this[_loadFSNode]({ path, root: this[_actualTree], useRootOverrides: true }) | ||
.then(node => this[_loadFSTree](node)) | ||
promises.push(p) | ||
} | ||
} | ||
await Promise.all(promises) | ||
} | ||
[_transplant] (root) { | ||
@@ -256,76 +267,59 @@ if (!root || root === this[_actualTree]) { | ||
[_loadFSNode] ({ path, parent, real, root, loadOverrides, useRootOverrides }) { | ||
async [_loadFSNode] ({ path, parent, real, root, loadOverrides, useRootOverrides }) { | ||
if (!real) { | ||
return realpath(path, this[_rpcache], this[_stcache]) | ||
.then( | ||
real => this[_loadFSNode]({ | ||
path, | ||
parent, | ||
real, | ||
root, | ||
loadOverrides, | ||
useRootOverrides, | ||
}), | ||
// if realpath fails, just provide a dummy error node | ||
error => new Node({ | ||
error, | ||
path, | ||
realpath: path, | ||
parent, | ||
root, | ||
loadOverrides, | ||
}) | ||
) | ||
} | ||
// cache temporarily holds a promise placeholder so we don't try to create | ||
// the same node multiple times. this is rare to encounter, given the | ||
// aggressive caching on realpath and lstat calls, but it's possible that | ||
// it's already loaded as a tree top, and then gets its parent loaded | ||
// later, if a symlink points deeper in the tree. | ||
const cached = this[_cache].get(path) | ||
if (cached && !cached.dummy) { | ||
return Promise.resolve(cached).then(node => { | ||
node.parent = parent | ||
return node | ||
}) | ||
} | ||
const p = rpj(join(real, 'package.json')) | ||
// soldier on if read-package-json raises an error | ||
.then(pkg => [pkg, null], error => [null, error]) | ||
.then(([pkg, error]) => { | ||
return this[normalize(path) === real ? _newNode : _newLink]({ | ||
installLinks: this.installLinks, | ||
legacyPeerDeps: this.legacyPeerDeps, | ||
try { | ||
real = await realpath(path, this[_rpcache], this[_stcache]) | ||
} catch (error) { | ||
// if realpath fails, just provide a dummy error node | ||
return new Node({ | ||
error, | ||
path, | ||
realpath: real, | ||
pkg, | ||
error, | ||
realpath: path, | ||
parent, | ||
root, | ||
loadOverrides, | ||
...(useRootOverrides && root.overrides | ||
? { overrides: root.overrides.getNodeRule({ name: pkg.name, version: pkg.version }) } | ||
: {}), | ||
}) | ||
}) | ||
.then(node => { | ||
this[_cache].set(path, node) | ||
return node | ||
}) | ||
} | ||
} | ||
this[_cache].set(path, p) | ||
return p | ||
const cached = this[_cache].get(path) | ||
let node | ||
// missing edges get a dummy node, assign the parent and return it | ||
if (cached && !cached.dummy) { | ||
cached.parent = parent | ||
return cached | ||
} else { | ||
const params = { | ||
installLinks: this.installLinks, | ||
legacyPeerDeps: this.legacyPeerDeps, | ||
path, | ||
realpath: real, | ||
parent, | ||
root, | ||
loadOverrides, | ||
} | ||
try { | ||
const pkg = await rpj(join(real, 'package.json')) | ||
params.pkg = pkg | ||
if (useRootOverrides && root.overrides) { | ||
params.overrides = root.overrides.getNodeRule({ name: pkg.name, version: pkg.version }) | ||
} | ||
} catch (err) { | ||
params.error = err | ||
} | ||
// soldier on if read-package-json raises an error, passing it to the | ||
// Node which will attach it to its errors array (Link passes it along to | ||
// its target node) | ||
if (normalize(path) === real) { | ||
node = this[_newNode](params) | ||
} else { | ||
node = await this[_newLink](params) | ||
} | ||
} | ||
this[_cache].set(path, node) | ||
return node | ||
} | ||
// this is the way it is to expose a timing issue which is difficult to | ||
// test otherwise. The creation of a Node may take slightly longer than | ||
// the creation of a Link that targets it. If the Node has _begun_ its | ||
// creation phase (and put a Promise in the cache) then the Link will | ||
// get a Promise as its cachedTarget instead of an actual Node object. | ||
// This is not a problem, because it gets resolved prior to returning | ||
// the tree or attempting to load children. However, it IS remarkably | ||
// difficult to get to happen in a test environment to verify reliably. | ||
// Hence this kludge. | ||
[_newNode] (options) { | ||
@@ -339,8 +333,6 @@ // check it for an fsParent if it's a tree top. there's a decent chance | ||
} | ||
return process.env._TEST_ARBORIST_SLOW_LINK_TARGET_ === '1' | ||
? new Promise(res => setTimeout(() => res(new Node(options)), 100)) | ||
: new Node(options) | ||
return new Node(options) | ||
} | ||
[_newLink] (options) { | ||
async [_newLink] (options) { | ||
const { realpath } = options | ||
@@ -352,9 +344,7 @@ this[_topNodes].add(realpath) | ||
if (!target) { | ||
// Link set its target itself in this case | ||
this[_cache].set(realpath, link.target) | ||
// if a link target points at a node outside of the root tree's | ||
// node_modules hierarchy, then load that node as well. | ||
return this[_loadFSTree](link.target).then(() => link) | ||
} else if (target.then) { | ||
// eslint-disable-next-line promise/catch-or-return | ||
target.then(node => link.target = node) | ||
await this[_loadFSTree](link.target) | ||
} | ||
@@ -365,24 +355,13 @@ | ||
[_loadFSTree] (node) { | ||
async [_loadFSTree] (node) { | ||
const did = this[_actualTreeLoaded] | ||
node = node.target | ||
// if a Link target has started, but not completed, then | ||
// a Promise will be in the cache to indicate this. | ||
if (node.then) { | ||
return node.then(node => this[_loadFSTree](node)) | ||
if (!did.has(node.target.realpath)) { | ||
did.add(node.target.realpath) | ||
await this[_loadFSChildren](node.target) | ||
return Promise.all( | ||
[...node.target.children.entries()] | ||
.filter(([name, kid]) => !did.has(kid.realpath)) | ||
.map(([name, kid]) => this[_loadFSTree](kid)) | ||
) | ||
} | ||
// impossible except in pathological ELOOP cases | ||
/* istanbul ignore if */ | ||
if (did.has(node.realpath)) { | ||
return Promise.resolve(node) | ||
} | ||
did.add(node.realpath) | ||
return this[_loadFSChildren](node) | ||
.then(() => Promise.all( | ||
[...node.children.entries()] | ||
.filter(([name, kid]) => !did.has(kid.realpath)) | ||
.map(([name, kid]) => this[_loadFSTree](kid)))) | ||
} | ||
@@ -392,7 +371,8 @@ | ||
// and attach them to the node as a parent | ||
[_loadFSChildren] (node) { | ||
async [_loadFSChildren] (node) { | ||
const nm = resolve(node.realpath, 'node_modules') | ||
return readdir(nm).then(kids => { | ||
try { | ||
const kids = await readdir(nm) | ||
return Promise.all( | ||
// ignore . dirs and retired scoped package folders | ||
// ignore . dirs and retired scoped package folders | ||
kids.filter(kid => !/^(@[^/]+\/)?\./.test(kid)) | ||
@@ -404,5 +384,5 @@ .filter(kid => this[_filter](node, kid)) | ||
}))) | ||
}, | ||
// error in the readdir is not fatal, just means no kids | ||
() => {}) | ||
} catch { | ||
// error in the readdir is not fatal, just means no kids | ||
} | ||
} | ||
@@ -454,2 +434,3 @@ | ||
: new Node({ path: p, root: node.root, dummy: true }) | ||
// not a promise | ||
this[_cache].set(p, d) | ||
@@ -475,20 +456,2 @@ if (d.dummy) { | ||
} | ||
// try to find a node that is the parent in a fs tree sense, but not a | ||
// node_modules tree sense, of any link targets. this allows us to | ||
// resolve deps that node will find, but a legacy npm view of the | ||
// world would not have noticed. | ||
[_findFSParents] () { | ||
for (const path of this[_topNodes]) { | ||
const node = this[_cache].get(path) | ||
if (node && !node.parent && !node.fsParent) { | ||
for (const p of walkUp(dirname(path))) { | ||
if (this[_cache].has(p)) { | ||
node.fsParent = this[_cache].get(p) | ||
break | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
@@ -112,4 +112,4 @@ // Do not rely on package._fields, so that we don't throw | ||
// if we're installing links and the node is a link, then it's invalid because we want | ||
// a real node to be there | ||
if (requestor.installLinks) { | ||
// a real node to be there. Except for workspaces. They are always links. | ||
if (requestor.installLinks && !child.isWorkspace) { | ||
return !isLink | ||
@@ -116,0 +116,0 @@ } |
@@ -1,2 +0,1 @@ | ||
const debug = require('./debug.js') | ||
const relpath = require('./relpath.js') | ||
@@ -56,23 +55,2 @@ const Node = require('./node.js') | ||
if (current && current.then) { | ||
debug(() => { | ||
throw Object.assign(new Error('cannot set target while awaiting'), { | ||
path: this.path, | ||
realpath: this.realpath, | ||
}) | ||
}) | ||
} | ||
if (target && target.then) { | ||
// can set to a promise during an async tree build operation | ||
// wait until then to assign it. | ||
this[_target] = target | ||
// eslint-disable-next-line promise/always-return, promise/catch-or-return | ||
target.then(node => { | ||
this[_target] = null | ||
this.target = node | ||
}) | ||
return | ||
} | ||
if (!target) { | ||
@@ -79,0 +57,0 @@ if (current && current.linksIn) { |
@@ -6,4 +6,5 @@ 'use strict' | ||
const localeCompare = require('@isaacs/string-locale-compare')('en') | ||
const log = require('proc-log') | ||
const minimatch = require('minimatch') | ||
const npa = require('npm-package-arg') | ||
const minimatch = require('minimatch') | ||
const semver = require('semver') | ||
@@ -295,7 +296,111 @@ | ||
semverPseudo () { | ||
if (!this.currentAstNode.semverValue) { | ||
const { | ||
attributeMatcher, | ||
lookupProperties, | ||
semverFunc = 'infer', | ||
semverValue, | ||
} = this.currentAstNode | ||
const { qualifiedAttribute } = attributeMatcher | ||
if (!semverValue) { | ||
// DEPRECATED: remove this warning and throw an error as part of @npmcli/arborist@6 | ||
log.warn('query', 'usage of :semver() with no parameters is deprecated') | ||
return this.initialItems | ||
} | ||
return this.initialItems.filter(node => | ||
semver.satisfies(node.version, this.currentAstNode.semverValue)) | ||
if (!semver.valid(semverValue) && !semver.validRange(semverValue)) { | ||
throw Object.assign( | ||
new Error(`\`${semverValue}\` is not a valid semver version or range`), | ||
{ code: 'EQUERYINVALIDSEMVER' }) | ||
} | ||
const valueIsVersion = !!semver.valid(semverValue) | ||
const nodeMatches = (node, obj) => { | ||
// if we already have an operator, the user provided some test as part of the selector | ||
// we evaluate that first because if it fails we don't want this node anyway | ||
if (attributeMatcher.operator) { | ||
if (!attributeMatch(attributeMatcher, obj)) { | ||
// if the initial operator doesn't match, we're done | ||
return false | ||
} | ||
} | ||
const attrValue = obj[qualifiedAttribute] | ||
// both valid and validRange return null for undefined, so this will skip both nodes that | ||
// do not have the attribute defined as well as those where the attribute value is invalid | ||
// and those where the value from the package.json is not a string | ||
if ((!semver.valid(attrValue) && !semver.validRange(attrValue)) || | ||
typeof attrValue !== 'string') { | ||
return false | ||
} | ||
const attrIsVersion = !!semver.valid(attrValue) | ||
let actualFunc = semverFunc | ||
// if we're asked to infer, we examine outputs to make a best guess | ||
if (actualFunc === 'infer') { | ||
if (valueIsVersion && attrIsVersion) { | ||
// two versions -> semver.eq | ||
actualFunc = 'eq' | ||
} else if (!valueIsVersion && !attrIsVersion) { | ||
// two ranges -> semver.intersects | ||
actualFunc = 'intersects' | ||
} else { | ||
// anything else -> semver.satisfies | ||
actualFunc = 'satisfies' | ||
} | ||
} | ||
if (['eq', 'neq', 'gt', 'gte', 'lt', 'lte'].includes(actualFunc)) { | ||
// both sides must be versions, but one is not | ||
if (!valueIsVersion || !attrIsVersion) { | ||
return false | ||
} | ||
return semver[actualFunc](attrValue, semverValue) | ||
} else if (['gtr', 'ltr', 'satisfies'].includes(actualFunc)) { | ||
// at least one side must be a version, but neither is | ||
if (!valueIsVersion && !attrIsVersion) { | ||
return false | ||
} | ||
return valueIsVersion | ||
? semver[actualFunc](semverValue, attrValue) | ||
: semver[actualFunc](attrValue, semverValue) | ||
} else if (['intersects', 'subset'].includes(actualFunc)) { | ||
// these accept two ranges and since a version is also a range, anything goes | ||
return semver[actualFunc](attrValue, semverValue) | ||
} else { | ||
// user provided a function we don't know about, throw an error | ||
throw Object.assign(new Error(`\`semver.${actualFunc}\` is not a supported operator.`), | ||
{ code: 'EQUERYINVALIDOPERATOR' }) | ||
} | ||
} | ||
return this.initialItems.filter((node) => { | ||
// no lookupProperties just means its a top level property, see if it matches | ||
if (!lookupProperties.length) { | ||
return nodeMatches(node, node.package) | ||
} | ||
// this code is mostly duplicated from attrPseudo to traverse into the package until we get | ||
// to our deepest requested object | ||
let objs = [node.package] | ||
for (const prop of lookupProperties) { | ||
if (prop === arrayDelimiter) { | ||
objs = objs.flat() | ||
continue | ||
} | ||
objs = objs.flatMap(obj => obj[prop] || []) | ||
const noAttr = objs.every(obj => !obj) | ||
if (noAttr) { | ||
return false | ||
} | ||
return objs.some(obj => nodeMatches(node, obj)) | ||
} | ||
}) | ||
} | ||
@@ -363,2 +468,3 @@ | ||
} | ||
return attributeOperators[operator]({ | ||
@@ -365,0 +471,0 @@ attr, |
{ | ||
"name": "@npmcli/arborist", | ||
"version": "5.6.1", | ||
"version": "6.0.0-pre.0", | ||
"description": "Manage node_modules trees", | ||
@@ -45,3 +45,3 @@ "dependencies": { | ||
"@npmcli/eslint-config": "^3.1.0", | ||
"@npmcli/template-oss": "3.8.0", | ||
"@npmcli/template-oss": "4.0.0", | ||
"benchmark": "^2.1.4", | ||
@@ -60,5 +60,2 @@ "chalk": "^4.1.0", | ||
"test-proxy": "ARBORIST_TEST_PROXY=1 tap --snapshot", | ||
"preversion": "npm test", | ||
"postversion": "npm publish", | ||
"prepublishOnly": "git push origin --follow-tags", | ||
"eslint": "eslint", | ||
@@ -104,8 +101,8 @@ "lint": "eslint \"**/*.js\"", | ||
"engines": { | ||
"node": "^12.13.0 || ^14.15.0 || >=16.0.0" | ||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" | ||
}, | ||
"templateOSS": { | ||
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.", | ||
"version": "3.8.0" | ||
"version": "4.0.0" | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
437149
11572
16
2