@npmcli/arborist 5.5.0 → 6.0.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69,7 +69,6 @@ const _symlink = Symbol('symlink')
69
69
  const _warnDeprecated = Symbol('warnDeprecated')
70
70
  const _loadBundlesAndUpdateTrees = Symbol.for('loadBundlesAndUpdateTrees')
71
71
  const _submitQuickAudit = Symbol('submitQuickAudit')
72
- const _awaitQuickAudit = Symbol('awaitQuickAudit')
73
72
  const _unpackNewModules = Symbol.for('unpackNewModules')
74
73
  const _moveContents = Symbol.for('moveContents')
75
74
  const _moveBackRetiredUnchanged = Symbol.for('moveBackRetiredUnchanged')
@@ -156,7 +155,8 @@ module.exports = cls => class Reifier extends cls {
156
155
  await this[_reifyPackages]()
157
156
  await this[_saveIdealTree](options)
158
157
  await this[_copyIdealToActual]()
159
- await this[_awaitQuickAudit]()
158
+ // This is a very bad pattern and I can't wait to stop doing it
159
+ this.auditReport = await this.auditReport
160
160
 
161
161
  this.finishTracker('reify')
162
162
  process.emit('timeEnd', 'reify')
@@ -531,12 +531,12 @@ module.exports = cls => class Reifier extends cls {
531
531
  const targets = [...roots, ...Object.keys(this[_retiredPaths])]
532
532
  const unlinks = targets
533
533
  .map(path => rimraf(path).catch(er => failures.push([path, er])))
534
- return promiseAllRejectLate(unlinks)
535
- .then(() => {
536
- if (failures.length) {
537
- log.warn('cleanup', 'Failed to remove some directories', failures)
538
- }
539
- })
534
+ return promiseAllRejectLate(unlinks).then(() => {
535
+ // eslint-disable-next-line promise/always-return
536
+ if (failures.length) {
537
+ log.warn('cleanup', 'Failed to remove some directories', failures)
538
+ }
539
+ })
540
540
  .then(() => process.emit('timeEnd', 'reify:rollback:createSparse'))
541
541
  .then(() => this[_rollbackRetireShallowNodes](er))
542
542
  }
@@ -592,21 +592,21 @@ module.exports = cls => class Reifier extends cls {
592
592
  this.addTracker('reify', node.name, node.location)
593
593
 
594
594
  const { npmVersion, nodeVersion } = this.options
595
- const p = Promise.resolve()
596
- .then(async () => {
597
- // when we reify an optional node, check the engine and platform
598
- // first. be sure to ignore the --force and --engine-strict flags,
599
- // since we always want to skip any optional packages we can't install.
600
- // these checks throwing will result in a rollback and removal
601
- // of the mismatches
602
- if (node.optional) {
603
- checkEngine(node.package, npmVersion, nodeVersion, false)
604
- checkPlatform(node.package, false)
605
- }
606
- await this[_checkBins](node)
607
- await this[_extractOrLink](node)
608
- await this[_warnDeprecated](node)
609
- })
595
+ const p = Promise.resolve().then(async () => {
596
+ // when we reify an optional node, check the engine and platform
597
+ // first. be sure to ignore the --force and --engine-strict flags,
598
+ // since we always want to skip any optional packages we can't install.
599
+ // these checks throwing will result in a rollback and removal
600
+ // of the mismatches
601
+ // eslint-disable-next-line promise/always-return
602
+ if (node.optional) {
603
+ checkEngine(node.package, npmVersion, nodeVersion, false)
604
+ checkPlatform(node.package, false)
605
+ }
606
+ await this[_checkBins](node)
607
+ await this[_extractOrLink](node)
608
+ await this[_warnDeprecated](node)
609
+ })
610
610
 
611
611
  return this[_handleOptionalFailure](node, p)
612
612
  .then(() => {
@@ -916,9 +916,10 @@ module.exports = cls => class Reifier extends cls {
916
916
  }
917
917
  }
918
918
 
919
- [_submitQuickAudit] () {
919
+ async [_submitQuickAudit] () {
920
920
  if (this.options.audit === false) {
921
- return this.auditReport = null
921
+ this.auditReport = null
922
+ return
922
923
  }
923
924
 
924
925
  // we submit the quick audit at this point in the process, as soon as
@@ -940,16 +941,10 @@ module.exports = cls => class Reifier extends cls {
940
941
  )
941
942
  }
942
943
 
943
- this.auditReport = AuditReport.load(tree, options)
944
- .then(res => {
945
- process.emit('timeEnd', 'reify:audit')
946
- this.auditReport = res
947
- })
948
- }
949
-
950
- // return the promise if we're waiting for it, or the replaced result
951
- [_awaitQuickAudit] () {
952
- return this.auditReport
944
+ this.auditReport = AuditReport.load(tree, options).then(res => {
945
+ process.emit('timeEnd', 'reify:audit')
946
+ return res
947
+ })
953
948
  }
954
949
 
955
950
  // ok! actually unpack stuff into their target locations!
@@ -1126,7 +1121,7 @@ module.exports = cls => class Reifier extends cls {
1126
1121
  // remove the retired folders, and any deleted nodes
1127
1122
  // If this fails, there isn't much we can do but tell the user about it.
1128
1123
  // Thankfully, it's pretty unlikely that it'll fail, since rimraf is a tank.
1129
- [_removeTrash] () {
1124
+ async [_removeTrash] () {
1130
1125
  process.emit('time', 'reify:trash')
1131
1126
  const promises = []
1132
1127
  const failures = []
@@ -1136,12 +1131,11 @@ module.exports = cls => class Reifier extends cls {
1136
1131
  promises.push(rm(path))
1137
1132
  }
1138
1133
 
1139
- return promiseAllRejectLate(promises).then(() => {
1140
- if (failures.length) {
1141
- log.warn('cleanup', 'Failed to remove some directories', failures)
1142
- }
1143
- })
1144
- .then(() => process.emit('timeEnd', 'reify:trash'))
1134
+ await promiseAllRejectLate(promises)
1135
+ if (failures.length) {
1136
+ log.warn('cleanup', 'Failed to remove some directories', failures)
1137
+ }
1138
+ process.emit('timeEnd', 'reify:trash')
1145
1139
  }
1146
1140
 
1147
1141
  // last but not least, we save the ideal tree metadata to the package-lock
@@ -1302,7 +1296,9 @@ module.exports = cls => class Reifier extends cls {
1302
1296
  if (semver.subset(edge.spec, node.version)) {
1303
1297
  return false
1304
1298
  }
1305
- } catch {}
1299
+ } catch {
1300
+ // ignore errors
1301
+ }
1306
1302
  }
1307
1303
  return true
1308
1304
  }
@@ -175,7 +175,9 @@ class AuditReport extends Map {
175
175
  } else {
176
176
  // calculate a metavuln, if necessary
177
177
  const calc = this.calculator.calculate(dep.packageName, advisory)
178
+ // eslint-disable-next-line promise/always-return
178
179
  p.push(calc.then(meta => {
180
+ // eslint-disable-next-line promise/always-return
179
181
  if (meta.testVersion(dep.version, spec)) {
180
182
  advisories.add(meta)
181
183
  }
package/lib/dep-valid.js CHANGED
@@ -109,8 +109,8 @@ const depValid = (child, requested, requestor) => {
109
109
  const linkValid = (child, requested, requestor) => {
110
110
  const isLink = !!child.isLink
111
111
  // if we're installing links and the node is a link, then it's invalid because we want
112
- // a real node to be there
113
- if (requestor.installLinks) {
112
+ // a real node to be there. Except for workspaces. They are always links.
113
+ if (requestor.installLinks && !child.isWorkspace) {
114
114
  return !isLink
115
115
  }
116
116
 
package/lib/link.js CHANGED
@@ -1,4 +1,3 @@
1
- const debug = require('./debug.js')
2
1
  const relpath = require('./relpath.js')
3
2
  const Node = require('./node.js')
4
3
  const _loadDeps = Symbol.for('Arborist.Node._loadDeps')
@@ -53,26 +52,6 @@ class Link extends Node {
53
52
  return
54
53
  }
55
54
 
56
- if (current && current.then) {
57
- debug(() => {
58
- throw Object.assign(new Error('cannot set target while awaiting'), {
59
- path: this.path,
60
- realpath: this.realpath,
61
- })
62
- })
63
- }
64
-
65
- if (target && target.then) {
66
- // can set to a promise during an async tree build operation
67
- // wait until then to assign it.
68
- this[_target] = target
69
- target.then(node => {
70
- this[_target] = null
71
- this.target = node
72
- })
73
- return
74
- }
75
-
76
55
  if (!target) {
77
56
  if (current && current.linksIn) {
78
57
  current.linksIn.delete(this)
package/lib/node.js CHANGED
@@ -334,6 +334,10 @@ class Node {
334
334
  return `${myname}@${alias}${version}`
335
335
  }
336
336
 
337
+ get overridden () {
338
+ return !!(this.overrides && this.overrides.value && this.overrides.name === this.name)
339
+ }
340
+
337
341
  get package () {
338
342
  return this[_package]
339
343
  }
@@ -560,7 +564,8 @@ class Node {
560
564
  // this allows us to do new Node({...}) and then set the root later.
561
565
  // just make the assignment so we don't lose it, and move on.
562
566
  if (!this.path || !root.realpath || !root.path) {
563
- return this[_root] = root
567
+ this[_root] = root
568
+ return
564
569
  }
565
570
 
566
571
  // temporarily become a root node
@@ -3,8 +3,9 @@
3
3
  const { resolve } = require('path')
4
4
  const { parser, arrayDelimiter } = require('@npmcli/query')
5
5
  const localeCompare = require('@isaacs/string-locale-compare')('en')
6
- const npa = require('npm-package-arg')
6
+ const log = require('proc-log')
7
7
  const minimatch = require('minimatch')
8
+ const npa = require('npm-package-arg')
8
9
  const semver = require('semver')
9
10
 
10
11
  // handle results for parsed query asts, results are stored in a map that has a
@@ -262,6 +263,10 @@ class Results {
262
263
  !internalSelector.has(node))
263
264
  }
264
265
 
266
+ overriddenPseudo () {
267
+ return this.initialItems.filter(node => node.overridden)
268
+ }
269
+
265
270
  pathPseudo () {
266
271
  return this.initialItems.filter(node => {
267
272
  if (!this.currentAstNode.pathValue) {
@@ -287,11 +292,115 @@ class Results {
287
292
  }
288
293
 
289
294
  semverPseudo () {
290
- if (!this.currentAstNode.semverValue) {
295
+ const {
296
+ attributeMatcher,
297
+ lookupProperties,
298
+ semverFunc = 'infer',
299
+ semverValue,
300
+ } = this.currentAstNode
301
+ const { qualifiedAttribute } = attributeMatcher
302
+
303
+ if (!semverValue) {
304
+ // DEPRECATED: remove this warning and throw an error as part of @npmcli/arborist@6
305
+ log.warn('query', 'usage of :semver() with no parameters is deprecated')
291
306
  return this.initialItems
292
307
  }
293
- return this.initialItems.filter(node =>
294
- semver.satisfies(node.version, this.currentAstNode.semverValue))
308
+
309
+ if (!semver.valid(semverValue) && !semver.validRange(semverValue)) {
310
+ throw Object.assign(
311
+ new Error(`\`${semverValue}\` is not a valid semver version or range`),
312
+ { code: 'EQUERYINVALIDSEMVER' })
313
+ }
314
+
315
+ const valueIsVersion = !!semver.valid(semverValue)
316
+
317
+ const nodeMatches = (node, obj) => {
318
+ // if we already have an operator, the user provided some test as part of the selector
319
+ // we evaluate that first because if it fails we don't want this node anyway
320
+ if (attributeMatcher.operator) {
321
+ if (!attributeMatch(attributeMatcher, obj)) {
322
+ // if the initial operator doesn't match, we're done
323
+ return false
324
+ }
325
+ }
326
+
327
+ const attrValue = obj[qualifiedAttribute]
328
+ // both valid and validRange return null for undefined, so this will skip both nodes that
329
+ // do not have the attribute defined as well as those where the attribute value is invalid
330
+ // and those where the value from the package.json is not a string
331
+ if ((!semver.valid(attrValue) && !semver.validRange(attrValue)) ||
332
+ typeof attrValue !== 'string') {
333
+ return false
334
+ }
335
+
336
+ const attrIsVersion = !!semver.valid(attrValue)
337
+
338
+ let actualFunc = semverFunc
339
+
340
+ // if we're asked to infer, we examine outputs to make a best guess
341
+ if (actualFunc === 'infer') {
342
+ if (valueIsVersion && attrIsVersion) {
343
+ // two versions -> semver.eq
344
+ actualFunc = 'eq'
345
+ } else if (!valueIsVersion && !attrIsVersion) {
346
+ // two ranges -> semver.intersects
347
+ actualFunc = 'intersects'
348
+ } else {
349
+ // anything else -> semver.satisfies
350
+ actualFunc = 'satisfies'
351
+ }
352
+ }
353
+
354
+ if (['eq', 'neq', 'gt', 'gte', 'lt', 'lte'].includes(actualFunc)) {
355
+ // both sides must be versions, but one is not
356
+ if (!valueIsVersion || !attrIsVersion) {
357
+ return false
358
+ }
359
+
360
+ return semver[actualFunc](attrValue, semverValue)
361
+ } else if (['gtr', 'ltr', 'satisfies'].includes(actualFunc)) {
362
+ // at least one side must be a version, but neither is
363
+ if (!valueIsVersion && !attrIsVersion) {
364
+ return false
365
+ }
366
+
367
+ return valueIsVersion
368
+ ? semver[actualFunc](semverValue, attrValue)
369
+ : semver[actualFunc](attrValue, semverValue)
370
+ } else if (['intersects', 'subset'].includes(actualFunc)) {
371
+ // these accept two ranges and since a version is also a range, anything goes
372
+ return semver[actualFunc](attrValue, semverValue)
373
+ } else {
374
+ // user provided a function we don't know about, throw an error
375
+ throw Object.assign(new Error(`\`semver.${actualFunc}\` is not a supported operator.`),
376
+ { code: 'EQUERYINVALIDOPERATOR' })
377
+ }
378
+ }
379
+
380
+ return this.initialItems.filter((node) => {
381
+ // no lookupProperties just means its a top level property, see if it matches
382
+ if (!lookupProperties.length) {
383
+ return nodeMatches(node, node.package)
384
+ }
385
+
386
+ // this code is mostly duplicated from attrPseudo to traverse into the package until we get
387
+ // to our deepest requested object
388
+ let objs = [node.package]
389
+ for (const prop of lookupProperties) {
390
+ if (prop === arrayDelimiter) {
391
+ objs = objs.flat()
392
+ continue
393
+ }
394
+
395
+ objs = objs.flatMap(obj => obj[prop] || [])
396
+ const noAttr = objs.every(obj => !obj)
397
+ if (noAttr) {
398
+ return false
399
+ }
400
+
401
+ return objs.some(obj => nodeMatches(node, obj))
402
+ }
403
+ })
295
404
  }
296
405
 
297
406
  typePseudo () {
@@ -354,6 +463,7 @@ const attributeOperator = ({ attr, value, insensitive, operator }) => {
354
463
  if (insensitive) {
355
464
  attr = attr.toLowerCase()
356
465
  }
466
+
357
467
  return attributeOperators[operator]({
358
468
  attr,
359
469
  insensitive,
package/lib/shrinkwrap.js CHANGED
@@ -184,34 +184,32 @@ const assertNoNewer = async (path, data, lockTime, dir = path, seen = null) => {
184
184
  ? Promise.resolve([{ name: 'node_modules', isDirectory: () => true }])
185
185
  : readdir(parent, { withFileTypes: true })
186
186
 
187
- return children.catch(() => [])
188
- .then(ents => Promise.all(ents.map(async ent => {
189
- const child = resolve(parent, ent.name)
190
- if (ent.isDirectory() && !/^\./.test(ent.name)) {
191
- await assertNoNewer(path, data, lockTime, child, seen)
192
- } else if (ent.isSymbolicLink()) {
193
- const target = resolve(parent, await readlink(child))
194
- const tstat = await stat(target).catch(
195
- /* istanbul ignore next - windows */ () => null)
196
- seen.add(relpath(path, child))
197
- /* istanbul ignore next - windows cannot do this */
198
- if (tstat && tstat.isDirectory() && !seen.has(relpath(path, target))) {
199
- await assertNoNewer(path, data, lockTime, target, seen)
200
- }
201
- }
202
- })))
203
- .then(() => {
204
- if (dir !== path) {
205
- return
187
+ const ents = await children.catch(() => [])
188
+ await Promise.all(ents.map(async ent => {
189
+ const child = resolve(parent, ent.name)
190
+ if (ent.isDirectory() && !/^\./.test(ent.name)) {
191
+ await assertNoNewer(path, data, lockTime, child, seen)
192
+ } else if (ent.isSymbolicLink()) {
193
+ const target = resolve(parent, await readlink(child))
194
+ const tstat = await stat(target).catch(
195
+ /* istanbul ignore next - windows */ () => null)
196
+ seen.add(relpath(path, child))
197
+ /* istanbul ignore next - windows cannot do this */
198
+ if (tstat && tstat.isDirectory() && !seen.has(relpath(path, target))) {
199
+ await assertNoNewer(path, data, lockTime, target, seen)
206
200
  }
201
+ }
202
+ }))
203
+ if (dir !== path) {
204
+ return
205
+ }
207
206
 
208
- // assert that all the entries in the lockfile were seen
209
- for (const loc of new Set(Object.keys(data.packages))) {
210
- if (!seen.has(loc)) {
211
- throw 'missing from node_modules: ' + loc
212
- }
213
- }
214
- })
207
+ // assert that all the entries in the lockfile were seen
208
+ for (const loc of new Set(Object.keys(data.packages))) {
209
+ if (!seen.has(loc)) {
210
+ throw 'missing from node_modules: ' + loc
211
+ }
212
+ }
215
213
  }
216
214
 
217
215
  const _awaitingUpdate = Symbol('_awaitingUpdate')
@@ -261,7 +259,9 @@ class Shrinkwrap {
261
259
  s.lockfileVersion = json.lockfileVersion
262
260
  }
263
261
  }
264
- } catch (e) {}
262
+ } catch {
263
+ // ignore errors
264
+ }
265
265
 
266
266
  return s
267
267
  }
@@ -442,7 +442,7 @@ class Shrinkwrap {
442
442
  this.newline = newline !== undefined ? newline : this.newline
443
443
  }
444
444
 
445
- load () {
445
+ async load () {
446
446
  // we don't need to load package-lock.json except for top of tree nodes,
447
447
  // only npm-shrinkwrap.json.
448
448
  return this[_maybeRead]().then(([sw, lock, yarn]) => {
@@ -464,7 +464,9 @@ class Shrinkwrap {
464
464
  // ignore invalid yarn data. we'll likely clobber it later anyway.
465
465
  try {
466
466
  this.yarnLock.parse(yarn)
467
- } catch (_) {}
467
+ } catch {
468
+ // ignore errors
469
+ }
468
470
  }
469
471
 
470
472
  return data ? parseJSON(data) : {}
@@ -515,8 +517,10 @@ class Shrinkwrap {
515
517
  !(lock.lockfileVersion >= 2) && !lock.requires
516
518
 
517
519
  // load old lockfile deps into the packages listing
520
+ // eslint-disable-next-line promise/always-return
518
521
  if (lock.dependencies && !lock.packages) {
519
522
  return rpj(this.path + '/package.json').then(pkg => pkg, er => ({}))
523
+ // eslint-disable-next-line promise/always-return
520
524
  .then(pkg => {
521
525
  this[_loadAll]('', null, this.data)
522
526
  this[_fixDependencies](pkg)
@@ -19,7 +19,9 @@ const setup = fn => {
19
19
  for (const sig of signals) {
20
20
  try {
21
21
  process.removeListener(sig, sigListeners[sig])
22
- } catch (er) {}
22
+ } catch {
23
+ // ignore errors
24
+ }
23
25
  }
24
26
  process.removeListener('beforeExit', onBeforeExit)
25
27
  sigListeners.loaded = false
@@ -62,7 +64,9 @@ const setup = fn => {
62
64
  process.setMaxListeners(length + 1)
63
65
  }
64
66
  process.on(sig, sigListeners[sig])
65
- } catch (er) {}
67
+ } catch {
68
+ // ignore errors
69
+ }
66
70
  }
67
71
  sigListeners.loaded = true
68
72
 
@@ -21,10 +21,12 @@ const specFromLock = (name, lock, where) => {
21
21
  if (lock.resolved) {
22
22
  return npa.resolve(name, lock.resolved, where)
23
23
  }
24
- } catch (_) { }
24
+ } catch {
25
+ // ignore errors
26
+ }
25
27
  try {
26
28
  return npa.resolve(name, lock.version, where)
27
- } catch (_) {
29
+ } catch {
28
30
  return {}
29
31
  }
30
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "5.5.0",
3
+ "version": "6.0.0-pre.0",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",
@@ -11,10 +11,10 @@
11
11
  "@npmcli/name-from-folder": "^1.0.1",
12
12
  "@npmcli/node-gyp": "^2.0.0",
13
13
  "@npmcli/package-json": "^2.0.0",
14
- "@npmcli/query": "^1.1.1",
14
+ "@npmcli/query": "^1.2.0",
15
15
  "@npmcli/run-script": "^4.1.3",
16
- "bin-links": "^3.0.0",
17
- "cacache": "^16.0.6",
16
+ "bin-links": "^3.0.3",
17
+ "cacache": "^16.1.3",
18
18
  "common-ancestor-path": "^1.0.1",
19
19
  "json-parse-even-better-errors": "^2.3.1",
20
20
  "json-stringify-nice": "^1.1.4",
@@ -24,7 +24,7 @@
24
24
  "nopt": "^6.0.0",
25
25
  "npm-install-checks": "^5.0.0",
26
26
  "npm-package-arg": "^9.0.0",
27
- "npm-pick-manifest": "^7.0.0",
27
+ "npm-pick-manifest": "^7.0.2",
28
28
  "npm-registry-fetch": "^13.0.0",
29
29
  "npmlog": "^6.0.2",
30
30
  "pacote": "^13.6.1",
@@ -41,8 +41,8 @@
41
41
  "walk-up-path": "^1.0.0"
42
42
  },
43
43
  "devDependencies": {
44
- "@npmcli/eslint-config": "^3.0.1",
45
- "@npmcli/template-oss": "3.5.0",
44
+ "@npmcli/eslint-config": "^3.1.0",
45
+ "@npmcli/template-oss": "4.0.0",
46
46
  "benchmark": "^2.1.4",
47
47
  "chalk": "^4.1.0",
48
48
  "minify-registry-metadata": "^2.1.0",
@@ -56,9 +56,6 @@
56
56
  "snap": "tap",
57
57
  "postsnap": "npm run lintfix",
58
58
  "test-proxy": "ARBORIST_TEST_PROXY=1 tap --snapshot",
59
- "preversion": "npm test",
60
- "postversion": "npm publish",
61
- "prepublishOnly": "git push origin --follow-tags",
62
59
  "eslint": "eslint",
63
60
  "lint": "eslint \"**/*.js\"",
64
61
  "lintfix": "npm run lint -- --fix",
@@ -99,10 +96,10 @@
99
96
  "timeout": "360"
100
97
  },
101
98
  "engines": {
102
- "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
99
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
103
100
  },
104
101
  "templateOSS": {
105
102
  "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
106
- "version": "3.5.0"
103
+ "version": "4.0.0"
107
104
  }
108
105
  }