@npmcli/arborist 9.1.1 → 9.1.3

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.
@@ -6,7 +6,7 @@ const pacote = require('pacote')
6
6
  const cacache = require('cacache')
7
7
  const { callLimit: promiseCallLimit } = require('promise-call-limit')
8
8
  const realpath = require('../../lib/realpath.js')
9
- const { resolve, dirname } = require('node:path')
9
+ const { resolve, dirname, sep } = require('node:path')
10
10
  const treeCheck = require('../tree-check.js')
11
11
  const { readdirScoped } = require('@npmcli/fs')
12
12
  const { lstat, readlink } = require('node:fs/promises')
@@ -192,9 +192,11 @@ module.exports = cls => class IdealTreeBuilder extends cls {
192
192
  }
193
193
 
194
194
  async #checkEngineAndPlatform () {
195
- const { engineStrict, npmVersion, nodeVersion } = this.options
195
+ const { engineStrict, npmVersion, nodeVersion, omit = [] } = this.options
196
+ const omitSet = new Set(omit)
197
+
196
198
  for (const node of this.idealTree.inventory.values()) {
197
- if (!node.optional) {
199
+ if (!node.optional && !node.shouldOmit(omitSet)) {
198
200
  try {
199
201
  // if devEngines is present in the root node we ignore the engines check
200
202
  if (!(node.isRoot && node.package.devEngines)) {
@@ -1224,9 +1226,21 @@ This is a one-time fix-up, please be patient...
1224
1226
  const { installLinks, legacyPeerDeps } = this
1225
1227
  const isWorkspace = this.idealTree.workspaces && this.idealTree.workspaces.has(spec.name)
1226
1228
 
1227
- // spec is a directory, link it unless installLinks is set or it's a workspace
1229
+ // spec is a directory, link it if:
1230
+ // - it's a workspace, OR
1231
+ // - it's a project-internal file: dependency (always linked), OR
1232
+ // - it's external and installLinks is false
1228
1233
  // TODO post arborist refactor, will need to check for installStrategy=linked
1229
- if (spec.type === 'directory' && (isWorkspace || !installLinks)) {
1234
+ let isProjectInternalFileSpec = false
1235
+ if (edge?.rawSpec.startsWith('file:../') || edge?.rawSpec.startsWith('file:./')) {
1236
+ const targetPath = resolve(parent.realpath, edge.rawSpec.slice(5))
1237
+ const resolvedProjectRoot = resolve(this.idealTree.realpath)
1238
+ // Check if the target is within the project root
1239
+ isProjectInternalFileSpec = targetPath.startsWith(resolvedProjectRoot + sep) || targetPath === resolvedProjectRoot
1240
+ }
1241
+ // Decide whether to link or copy the dependency
1242
+ const shouldLink = isWorkspace || isProjectInternalFileSpec || !installLinks
1243
+ if (spec.type === 'directory' && shouldLink) {
1230
1244
  return this.#linkFromSpec(name, spec, parent, edge)
1231
1245
  }
1232
1246
 
@@ -1476,11 +1490,6 @@ This is a one-time fix-up, please be patient...
1476
1490
  const needPrune = metaFromDisk && (mutateTree || flagsSuspect)
1477
1491
  if (this.#prune && needPrune) {
1478
1492
  this.#idealTreePrune()
1479
- for (const node of this.idealTree.inventory.values()) {
1480
- if (node.extraneous) {
1481
- node.parent = null
1482
- }
1483
- }
1484
1493
  }
1485
1494
 
1486
1495
  timeEnd()
@@ -1514,7 +1523,12 @@ This is a one-time fix-up, please be patient...
1514
1523
 
1515
1524
  #idealTreePrune () {
1516
1525
  for (const node of this.idealTree.inventory.values()) {
1517
- if (node.extraneous) {
1526
+ // optional peer dependencies are meant to be added to the tree
1527
+ // through an explicit required dependency (most commonly in the
1528
+ // root package.json), at which point they won't be optional so
1529
+ // any dependencies still marked as both optional and peer at
1530
+ // this point can be pruned as a special kind of extraneous
1531
+ if (node.extraneous || (node.peer && node.optional)) {
1518
1532
  node.parent = null
1519
1533
  }
1520
1534
  }
@@ -9,6 +9,7 @@ const debug = require('../debug.js')
9
9
  const { walkUp } = require('walk-up-path')
10
10
  const { log, time } = require('proc-log')
11
11
  const rpj = require('read-package-json-fast')
12
+ const hgi = require('hosted-git-info')
12
13
 
13
14
  const { dirname, resolve, relative, join } = require('node:path')
14
15
  const { depth: dfwalk } = require('treeverse')
@@ -83,9 +84,7 @@ module.exports = cls => class Reifier extends cls {
83
84
  #bundleUnpacked = new Set() // the nodes we unpack to read their bundles
84
85
  #dryRun
85
86
  #nmValidated = new Set()
86
- #omitDev
87
- #omitPeer
88
- #omitOptional
87
+ #omit
89
88
  #retiredPaths = {}
90
89
  #retiredUnchanged = {}
91
90
  #savePrefix
@@ -109,10 +108,7 @@ module.exports = cls => class Reifier extends cls {
109
108
  throw er
110
109
  }
111
110
 
112
- const omit = new Set(options.omit || [])
113
- this.#omitDev = omit.has('dev')
114
- this.#omitOptional = omit.has('optional')
115
- this.#omitPeer = omit.has('peer')
111
+ this.#omit = new Set(options.omit)
116
112
 
117
113
  // start tracker block
118
114
  this.addTracker('reify')
@@ -561,12 +557,11 @@ module.exports = cls => class Reifier extends cls {
561
557
  // adding to the trash list will skip reifying, and delete them
562
558
  // if they are currently in the tree and otherwise untouched.
563
559
  [_addOmitsToTrashList] () {
564
- if (!this.#omitDev && !this.#omitOptional && !this.#omitPeer) {
560
+ if (!this.#omit.size) {
565
561
  return
566
562
  }
567
563
 
568
564
  const timeEnd = time.start('reify:trashOmits')
569
-
570
565
  for (const node of this.idealTree.inventory.values()) {
571
566
  const { top } = node
572
567
 
@@ -582,12 +577,7 @@ module.exports = cls => class Reifier extends cls {
582
577
  }
583
578
 
584
579
  // omit node if the dep type matches any omit flags that were set
585
- if (
586
- node.peer && this.#omitPeer ||
587
- node.dev && this.#omitDev ||
588
- node.optional && this.#omitOptional ||
589
- node.devOptional && this.#omitOptional && this.#omitDev
590
- ) {
580
+ if (node.shouldOmit(this.#omit)) {
591
581
  this[_addNodeToTrashList](node)
592
582
  }
593
583
  }
@@ -886,7 +876,7 @@ module.exports = cls => class Reifier extends cls {
886
876
  // Shrinkwrap and Node classes carefully, so for now, just treat
887
877
  // the default reg as the magical animal that it has been.
888
878
  try {
889
- const resolvedURL = new URL(resolved)
879
+ const resolvedURL = hgi.parseUrl(resolved)
890
880
 
891
881
  if ((this.options.replaceRegistryHost === resolvedURL.hostname) ||
892
882
  this.options.replaceRegistryHost === 'always') {
@@ -1,5 +1,4 @@
1
1
  // an object representing the set of vulnerabilities in a tree
2
- /* eslint camelcase: "off" */
3
2
 
4
3
  const localeCompare = require('@isaacs/string-locale-compare')('en')
5
4
  const npa = require('npm-package-arg')
@@ -8,16 +7,15 @@ const pickManifest = require('npm-pick-manifest')
8
7
  const Vuln = require('./vuln.js')
9
8
  const Calculator = require('@npmcli/metavuln-calculator')
10
9
 
11
- const _getReport = Symbol('getReport')
12
- const _fixAvailable = Symbol('fixAvailable')
13
- const _checkTopNode = Symbol('checkTopNode')
14
- const _init = Symbol('init')
15
- const _omit = Symbol('omit')
16
10
  const { log, time } = require('proc-log')
17
11
 
18
12
  const npmFetch = require('npm-registry-fetch')
19
13
 
20
14
  class AuditReport extends Map {
15
+ #omit
16
+ error = null
17
+ topVulns = new Map()
18
+
21
19
  static load (tree, opts) {
22
20
  return new AuditReport(tree, opts).run()
23
21
  }
@@ -91,22 +89,18 @@ class AuditReport extends Map {
91
89
 
92
90
  constructor (tree, opts = {}) {
93
91
  super()
94
- const { omit } = opts
95
- this[_omit] = new Set(omit || [])
96
- this.topVulns = new Map()
97
-
92
+ this.#omit = new Set(opts.omit || [])
98
93
  this.calculator = new Calculator(opts)
99
- this.error = null
100
94
  this.options = opts
101
95
  this.tree = tree
102
96
  this.filterSet = opts.filterSet
103
97
  }
104
98
 
105
99
  async run () {
106
- this.report = await this[_getReport]()
100
+ this.report = await this.#getReport()
107
101
  log.silly('audit report', this.report)
108
102
  if (this.report) {
109
- await this[_init]()
103
+ await this.#init()
110
104
  }
111
105
  return this
112
106
  }
@@ -116,7 +110,7 @@ class AuditReport extends Map {
116
110
  return !!(vuln && vuln.isVulnerable(node))
117
111
  }
118
112
 
119
- async [_init] () {
113
+ async #init () {
120
114
  const timeEnd = time.start('auditReport:init')
121
115
 
122
116
  const promises = []
@@ -148,7 +142,7 @@ class AuditReport extends Map {
148
142
  if (!seen.has(k)) {
149
143
  const p = []
150
144
  for (const node of this.tree.inventory.query('packageName', name)) {
151
- if (!shouldAudit(node, this[_omit], this.filterSet)) {
145
+ if (!this.shouldAudit(node)) {
152
146
  continue
153
147
  }
154
148
 
@@ -171,7 +165,15 @@ class AuditReport extends Map {
171
165
  vuln.nodes.add(node)
172
166
  for (const { from: dep, spec } of node.edgesIn) {
173
167
  if (dep.isTop && !vuln.topNodes.has(dep)) {
174
- this[_checkTopNode](dep, vuln, spec)
168
+ vuln.fixAvailable = this.#fixAvailable(vuln, spec)
169
+ if (vuln.fixAvailable !== true) {
170
+ // now we know the top node is vulnerable, and cannot be
171
+ // upgraded out of the bad place without --force. But, there's
172
+ // no need to add it to the actual vulns list, because nothing
173
+ // depends on root.
174
+ this.topVulns.set(vuln.name, vuln)
175
+ vuln.topNodes.add(dep)
176
+ }
175
177
  } else {
176
178
  // calculate a metavuln, if necessary
177
179
  const calc = this.calculator.calculate(dep.packageName, advisory)
@@ -214,33 +216,14 @@ class AuditReport extends Map {
214
216
  timeEnd()
215
217
  }
216
218
 
217
- [_checkTopNode] (topNode, vuln, spec) {
218
- vuln.fixAvailable = this[_fixAvailable](topNode, vuln, spec)
219
-
220
- if (vuln.fixAvailable !== true) {
221
- // now we know the top node is vulnerable, and cannot be
222
- // upgraded out of the bad place without --force. But, there's
223
- // no need to add it to the actual vulns list, because nothing
224
- // depends on root.
225
- this.topVulns.set(vuln.name, vuln)
226
- vuln.topNodes.add(topNode)
227
- }
228
- }
229
-
230
- // check whether the top node is vulnerable.
231
- // check whether we can get out of the bad place with --force, and if
232
- // so, whether that update is SemVer Major
233
- [_fixAvailable] (topNode, vuln, spec) {
234
- // this will always be set to at least {name, versions:{}}
235
- const paku = vuln.packument
236
-
219
+ // given the spec, see if there is a fix available at all, and note whether or not it's a semver major fix or not (i.e. will need --force)
220
+ #fixAvailable (vuln, spec) {
221
+ // TODO we return true, false, OR an object here. this is probably a bad pattern.
237
222
  if (!vuln.testSpec(spec)) {
238
223
  return true
239
224
  }
240
225
 
241
- // similarly, even if we HAVE a packument, but we're looking for it
242
- // somewhere other than the registry, and we got something vulnerable,
243
- // then we're stuck with it.
226
+ // even if we HAVE a packument, if we're looking for it somewhere other than the registry and we have something vulnerable then we're stuck with it.
244
227
  const specObj = npa(spec)
245
228
  if (!specObj.registry) {
246
229
  return false
@@ -250,15 +233,13 @@ class AuditReport extends Map {
250
233
  spec = specObj.subSpec.rawSpec
251
234
  }
252
235
 
253
- // We don't provide fixes for top nodes other than root, but we
254
- // still check to see if the node is fixable with a different version,
255
- // and if that is a semver major bump.
236
+ // we don't provide fixes for top nodes other than root, but we still check to see if the node is fixable with a different version, and note if that is a semver major bump.
256
237
  try {
257
238
  const {
258
239
  _isSemVerMajor: isSemVerMajor,
259
240
  version,
260
241
  name,
261
- } = pickManifest(paku, spec, {
242
+ } = pickManifest(vuln.packument, spec, {
262
243
  ...this.options,
263
244
  before: null,
264
245
  avoid: vuln.range,
@@ -274,7 +255,7 @@ class AuditReport extends Map {
274
255
  throw new Error('do not call AuditReport.set() directly')
275
256
  }
276
257
 
277
- async [_getReport] () {
258
+ async #getReport () {
278
259
  // if we're not auditing, just return false
279
260
  if (this.options.audit === false || this.options.offline === true || this.tree.inventory.size === 1) {
280
261
  return null
@@ -282,7 +263,7 @@ class AuditReport extends Map {
282
263
 
283
264
  const timeEnd = time.start('auditReport:getReport')
284
265
  try {
285
- const body = prepareBulkData(this.tree, this[_omit], this.filterSet)
266
+ const body = this.prepareBulkData()
286
267
  log.silly('audit', 'bulk request', body)
287
268
 
288
269
  // no sense asking if we don't have anything to audit,
@@ -309,37 +290,39 @@ class AuditReport extends Map {
309
290
  timeEnd()
310
291
  }
311
292
  }
312
- }
313
-
314
- // return true if we should audit this one
315
- const shouldAudit = (node, omit, filterSet) =>
316
- !node.version ? false
317
- : node.isRoot ? false
318
- : filterSet && filterSet.size !== 0 && !filterSet.has(node) ? false
319
- : omit.size === 0 ? true
320
- : !( // otherwise, just ensure we're not omitting this one
321
- node.dev && omit.has('dev') ||
322
- node.optional && omit.has('optional') ||
323
- node.devOptional && omit.has('dev') && omit.has('optional') ||
324
- node.peer && omit.has('peer')
325
- )
326
-
327
- const prepareBulkData = (tree, omit, filterSet) => {
328
- const payload = {}
329
- for (const name of tree.inventory.query('packageName')) {
330
- const set = new Set()
331
- for (const node of tree.inventory.query('packageName', name)) {
332
- if (!shouldAudit(node, omit, filterSet)) {
333
- continue
334
- }
335
293
 
336
- set.add(node.version)
294
+ // return true if we should audit this one
295
+ shouldAudit (node) {
296
+ if (
297
+ !node.version ||
298
+ node.isRoot ||
299
+ (this.filterSet && this.filterSet?.size !== 0 && !this.filterSet?.has(node))
300
+ ) {
301
+ return false
337
302
  }
338
- if (set.size) {
339
- payload[name] = [...set]
303
+ if (this.#omit.size === 0) {
304
+ return true
305
+ }
306
+ return !node.shouldOmit(this.#omit)
307
+ }
308
+
309
+ prepareBulkData () {
310
+ const payload = {}
311
+ for (const name of this.tree.inventory.query('packageName')) {
312
+ const set = new Set()
313
+ for (const node of this.tree.inventory.query('packageName', name)) {
314
+ if (!this.shouldAudit(node)) {
315
+ continue
316
+ }
317
+
318
+ set.add(node.version)
319
+ }
320
+ if (set.size) {
321
+ payload[name] = [...set]
322
+ }
340
323
  }
324
+ return payload
341
325
  }
342
- return payload
343
326
  }
344
327
 
345
328
  module.exports = AuditReport
package/lib/node.js CHANGED
@@ -489,6 +489,15 @@ class Node {
489
489
  return false
490
490
  }
491
491
 
492
+ shouldOmit (omitSet) {
493
+ return (
494
+ this.peer && omitSet.has('peer') ||
495
+ this.dev && omitSet.has('dev') ||
496
+ this.optional && omitSet.has('optional') ||
497
+ this.devOptional && omitSet.has('optional') && omitSet.has('dev')
498
+ )
499
+ }
500
+
492
501
  getBundler (path = []) {
493
502
  // made a cycle, definitely not bundled!
494
503
  if (path.includes(this)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "9.1.1",
3
+ "version": "9.1.3",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",
@@ -41,7 +41,7 @@
41
41
  "devDependencies": {
42
42
  "@npmcli/eslint-config": "^5.0.1",
43
43
  "@npmcli/mock-registry": "^1.0.0",
44
- "@npmcli/template-oss": "4.23.6",
44
+ "@npmcli/template-oss": "4.24.4",
45
45
  "benchmark": "^2.1.4",
46
46
  "minify-registry-metadata": "^4.0.0",
47
47
  "nock": "^13.3.3",
@@ -93,7 +93,7 @@
93
93
  },
94
94
  "templateOSS": {
95
95
  "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
96
- "version": "4.23.6",
96
+ "version": "4.24.4",
97
97
  "content": "../../scripts/template-oss/index.js"
98
98
  }
99
99
  }