Socket
Socket
Sign inDemoInstall

@npmcli/arborist

Package Overview
Dependencies
Maintainers
5
Versions
192
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@npmcli/arborist - npm Package Compare versions

Comparing version 5.6.1 to 6.0.0-pre.0

427

lib/arborist/load-actual.js

@@ -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"
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc