@npmcli/arborist 2.1.0 → 2.2.2

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.
package/bin/actual.js ADDED
@@ -0,0 +1,21 @@
1
+ const Arborist = require('../')
2
+ const print = require('./lib/print-tree.js')
3
+ const options = require('./lib/options.js')
4
+ require('./lib/logging.js')
5
+ require('./lib/timers.js')
6
+
7
+ const start = process.hrtime()
8
+ new Arborist(options).loadActual(options).then(tree => {
9
+ const end = process.hrtime(start)
10
+ if (!process.argv.includes('--quiet'))
11
+ print(tree)
12
+
13
+ console.error(`read ${tree.inventory.size} deps in ${end[0] * 1000 + end[1] / 1e6}ms`)
14
+ if (options.save)
15
+ tree.meta.save()
16
+ if (options.saveHidden) {
17
+ tree.meta.hiddenLockfile = true
18
+ tree.meta.filename = options.path + '/node_modules/.package-lock.json'
19
+ tree.meta.save()
20
+ }
21
+ }).catch(er => console.error(er))
package/bin/audit.js ADDED
@@ -0,0 +1,48 @@
1
+ const Arborist = require('../')
2
+
3
+ const print = require('./lib/print-tree.js')
4
+ const options = require('./lib/options.js')
5
+ require('./lib/timers.js')
6
+ require('./lib/logging.js')
7
+
8
+ const Vuln = require('../lib/vuln.js')
9
+ const printReport = report => {
10
+ for (const vuln of report.values())
11
+ console.log(printVuln(vuln))
12
+ if (report.topVulns.size) {
13
+ console.log('\n# top-level vulnerabilities')
14
+ for (const vuln of report.topVulns.values())
15
+ console.log(printVuln(vuln))
16
+ }
17
+ }
18
+
19
+ const printVuln = vuln => {
20
+ return {
21
+ __proto__: { constructor: Vuln },
22
+ name: vuln.name,
23
+ issues: [...vuln.advisories].map(a => printAdvisory(a)),
24
+ range: vuln.simpleRange,
25
+ nodes: [...vuln.nodes].map(node => `${node.name} ${node.location || '#ROOT'}`),
26
+ ...(vuln.topNodes.size === 0 ? {} : {
27
+ topNodes: [...vuln.topNodes].map(node => `${node.location || '#ROOT'}`),
28
+ }),
29
+ }
30
+ }
31
+
32
+ const printAdvisory = a => `${a.title}${a.url ? ' ' + a.url : ''}`
33
+
34
+ const start = process.hrtime()
35
+ process.emit('time', 'audit script')
36
+ const arb = new Arborist(options)
37
+ arb.audit(options).then(tree => {
38
+ process.emit('timeEnd', 'audit script')
39
+ const end = process.hrtime(start)
40
+ if (options.fix)
41
+ print(tree)
42
+ if (!options.quiet)
43
+ printReport(arb.auditReport)
44
+ if (options.fix)
45
+ console.error(`resolved ${tree.inventory.size} deps in ${end[0] + end[1] / 1e9}s`)
46
+ if (tree.meta && options.save)
47
+ tree.meta.save()
48
+ }).catch(er => console.error(er))
package/bin/funding.js ADDED
@@ -0,0 +1,32 @@
1
+ const options = require('./lib/options.js')
2
+ require('./lib/logging.js')
3
+ require('./lib/timers.js')
4
+
5
+ const Arborist = require('../')
6
+ const a = new Arborist(options)
7
+ const query = options._.shift()
8
+ const start = process.hrtime()
9
+ a.loadVirtual().then(tree => {
10
+ // only load the actual tree if the virtual one doesn't have modern metadata
11
+ if (!tree.meta || !(tree.meta.originalLockfileVersion >= 2)) {
12
+ console.error('old metadata, load actual')
13
+ throw 'load actual'
14
+ } else {
15
+ console.error('meta ok, return virtual tree')
16
+ return tree
17
+ }
18
+ }).catch(() => a.loadActual()).then(tree => {
19
+ const end = process.hrtime(start)
20
+ if (!query) {
21
+ for (const node of tree.inventory.values()) {
22
+ if (node.package.funding)
23
+ console.log(node.name, node.location, node.package.funding)
24
+ }
25
+ } else {
26
+ for (const node of tree.inventory.query('name', query)) {
27
+ if (node.package.funding)
28
+ console.log(node.name, node.location, node.package.funding)
29
+ }
30
+ }
31
+ console.error(`read ${tree.inventory.size} deps in ${end[0] * 1000 + end[1] / 1e6}ms`)
32
+ })
package/bin/ideal.js ADDED
@@ -0,0 +1,20 @@
1
+ const Arborist = require('../')
2
+
3
+ const { inspect } = require('util')
4
+ const options = require('./lib/options.js')
5
+ const print = require('./lib/print-tree.js')
6
+ require('./lib/logging.js')
7
+ require('./lib/timers.js')
8
+
9
+ const start = process.hrtime()
10
+ new Arborist(options).buildIdealTree(options).then(tree => {
11
+ const end = process.hrtime(start)
12
+ print(tree)
13
+ console.error(`resolved ${tree.inventory.size} deps in ${end[0] + end[1] / 10e9}s`)
14
+ if (tree.meta && options.save)
15
+ tree.meta.save()
16
+ }).catch(er => {
17
+ const opt = { depth: Infinity, color: true }
18
+ console.error(er.code === 'ERESOLVE' ? inspect(er, opt) : er)
19
+ process.exitCode = 1
20
+ })
package/bin/index.js ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ const [cmd] = process.argv.splice(2, 1)
3
+
4
+ const usage = () => `Arborist - the npm tree doctor
5
+
6
+ Version: ${require('../package.json').version}
7
+
8
+ # USAGE
9
+ arborist <cmd> [path] [options...]
10
+
11
+ # COMMANDS
12
+
13
+ * reify: reify ideal tree to node_modules (install, update, rm, ...)
14
+ * ideal: generate and print the ideal tree
15
+ * actual: read and print the actual tree in node_modules
16
+ * virtual: read and print the virtual tree in the local shrinkwrap file
17
+ * shrinkwrap: load a local shrinkwrap and print its data
18
+ * audit: perform a security audit on project dependencies
19
+ * funding: query funding information in the local package tree. A second
20
+ positional argument after the path name can limit to a package name.
21
+ * license: query license information in the local package tree. A second
22
+ positional argument after the path name can limit to a license type.
23
+ * help: print this text
24
+
25
+ # OPTIONS
26
+
27
+ Most npm options are supported, but in camelCase rather than css-case. For
28
+ example, instead of '--dry-run', use '--dryRun'.
29
+
30
+ Additionally:
31
+
32
+ * --quiet will supppress the printing of package trees
33
+ * Instead of 'npm install <pkg>', use 'arborist reify --add=<pkg>'.
34
+ The '--add=<pkg>' option can be specified multiple times.
35
+ * Instead of 'npm rm <pkg>', use 'arborist reify --rm=<pkg>'.
36
+ The '--rm=<pkg>' option can be specified multiple times.
37
+ * Instead of 'npm update', use 'arborist reify --update-all'.
38
+ * 'npm audit fix' is 'arborist audit --fix'
39
+ `
40
+
41
+ const help = () => console.log(usage())
42
+
43
+ switch (cmd) {
44
+ case 'actual':
45
+ require('./actual.js')
46
+ break
47
+ case 'virtual':
48
+ require('./virtual.js')
49
+ break
50
+ case 'ideal':
51
+ require('./ideal.js')
52
+ break
53
+ case 'reify':
54
+ require('./reify.js')
55
+ break
56
+ case 'audit':
57
+ require('./audit.js')
58
+ break
59
+ case 'funding':
60
+ require('./funding.js')
61
+ break
62
+ case 'license':
63
+ require('./license.js')
64
+ break
65
+ case 'shrinkwrap':
66
+ require('./shrinkwrap.js')
67
+ break
68
+ case 'help':
69
+ case '-h':
70
+ case '--help':
71
+ help()
72
+ break
73
+ default:
74
+ process.exitCode = 1
75
+ console.error(usage())
76
+ break
77
+ }
@@ -0,0 +1,33 @@
1
+ const options = require('./options.js')
2
+ const { quiet = false } = options
3
+ const { loglevel = quiet ? 'warn' : 'silly' } = options
4
+
5
+ const levels = [
6
+ 'silly',
7
+ 'verbose',
8
+ 'info',
9
+ 'timing',
10
+ 'http',
11
+ 'notice',
12
+ 'warn',
13
+ 'error',
14
+ 'silent',
15
+ ]
16
+
17
+ const levelMap = new Map(levels.reduce((set, level, index) => {
18
+ set.push([level, index], [index, level])
19
+ return set
20
+ }, []))
21
+
22
+ const { inspect, format } = require('util')
23
+ if (loglevel !== 'silent') {
24
+ process.on('log', (level, ...args) => {
25
+ if (levelMap.get(level) < levelMap.get(loglevel))
26
+ return
27
+ const pref = `${process.pid} ${level} `
28
+ if (level === 'warn' && args[0] === 'ERESOLVE')
29
+ args[2] = inspect(args[2], { depth: 10 })
30
+ const msg = pref + format(...args).trim().split('\n').join(`\n${pref}`)
31
+ console.error(msg)
32
+ })
33
+ }
@@ -0,0 +1,49 @@
1
+ const options = module.exports = {
2
+ path: undefined,
3
+ cache: `${process.env.HOME}/.npm/_cacache`,
4
+ _: [],
5
+ }
6
+
7
+ for (const arg of process.argv.slice(2)) {
8
+ if (/^--add=/.test(arg)) {
9
+ options.add = options.add || []
10
+ options.add.push(arg.substr('--add='.length))
11
+ } else if (/^--rm=/.test(arg)) {
12
+ options.rm = options.rm || []
13
+ options.rm.push(arg.substr('--rm='.length))
14
+ } else if (arg === '--global')
15
+ options.global = true
16
+ else if (arg === '--global-style')
17
+ options.globalStyle = true
18
+ else if (arg === '--prefer-dedupe')
19
+ options.preferDedupe = true
20
+ else if (arg === '--legacy-peer-deps')
21
+ options.legacyPeerDeps = true
22
+ else if (arg === '--force')
23
+ options.force = true
24
+ else if (arg === '--update-all') {
25
+ options.update = options.update || {}
26
+ options.update.all = true
27
+ } else if (/^--update=/.test(arg)) {
28
+ options.update = options.update || {}
29
+ options.update.names = options.update.names || []
30
+ options.update.names.push(arg.substr('--update='.length))
31
+ } else if (/^--omit=/.test(arg)) {
32
+ options.omit = options.omit || []
33
+ options.omit.push(arg.substr('--omit='.length))
34
+ } else if (/^--[^=]+=/.test(arg)) {
35
+ const [key, ...v] = arg.replace(/^--/, '').split('=')
36
+ const val = v.join('=')
37
+ options[key] = val === 'false' ? false : val === 'true' ? true : val
38
+ } else if (/^--.+/.test(arg))
39
+ options[arg.replace(/^--/, '')] = true
40
+ else if (options.path === undefined)
41
+ options.path = arg
42
+ else
43
+ options._.push(arg)
44
+ }
45
+
46
+ if (options.path === undefined)
47
+ options.path = '.'
48
+
49
+ console.error(options)
@@ -0,0 +1,5 @@
1
+ const { inspect } = require('util')
2
+ const { quiet } = require('./options.js')
3
+
4
+ module.exports = quiet ? () => {}
5
+ : tree => console.log(inspect(tree.toJSON(), { depth: Infinity }))
@@ -0,0 +1,22 @@
1
+ const timers = Object.create(null)
2
+
3
+ process.on('time', name => {
4
+ if (timers[name])
5
+ throw new Error('conflicting timer! ' + name)
6
+ timers[name] = process.hrtime()
7
+ })
8
+
9
+ process.on('timeEnd', name => {
10
+ if (!timers[name])
11
+ throw new Error('timer not started! ' + name)
12
+ const res = process.hrtime(timers[name])
13
+ delete timers[name]
14
+ console.error(`${process.pid} ${name}`, res[0] * 1e3 + res[1] / 1e6)
15
+ })
16
+
17
+ process.on('exit', () => {
18
+ for (const name of Object.keys(timers)) {
19
+ console.error('Dangling timer: ', name)
20
+ process.exitCode = 1
21
+ }
22
+ })
package/bin/license.js ADDED
@@ -0,0 +1,34 @@
1
+ const Arborist = require('../')
2
+ const options = require('./lib/options.js')
3
+ require('./lib/logging.js')
4
+ require('./lib/timers.js')
5
+
6
+ const a = new Arborist(options)
7
+ const query = options._.shift()
8
+
9
+ a.loadVirtual().then(tree => {
10
+ // only load the actual tree if the virtual one doesn't have modern metadata
11
+ if (!tree.meta || !(tree.meta.originalLockfileVersion >= 2))
12
+ throw 'load actual'
13
+ else
14
+ return tree
15
+ }).catch((er) => {
16
+ console.error('loading actual tree', er)
17
+ return a.loadActual()
18
+ }).then(tree => {
19
+ if (!query) {
20
+ const set = []
21
+ for (const license of tree.inventory.query('license'))
22
+ set.push([tree.inventory.query('license', license).size, license])
23
+
24
+ for (const [count, license] of set.sort((a, b) =>
25
+ a[1] && b[1] ? b[0] - a[0] || a[1].localeCompare(b[1])
26
+ : a[1] ? -1
27
+ : b[1] ? 1
28
+ : 0))
29
+ console.log(count, license)
30
+ } else {
31
+ for (const node of tree.inventory.query('license', query === 'undefined' ? undefined : query))
32
+ console.log(`${node.name} ${node.location} ${node.package.description || ''}`)
33
+ }
34
+ })
package/bin/reify.js ADDED
@@ -0,0 +1,46 @@
1
+ const Arborist = require('../')
2
+
3
+ const options = require('./lib/options.js')
4
+ const print = require('./lib/print-tree.js')
5
+ require('./lib/logging.js')
6
+ require('./lib/timers.js')
7
+
8
+ const printDiff = diff => {
9
+ const {depth} = require('treeverse')
10
+ depth({
11
+ tree: diff,
12
+ visit: d => {
13
+ if (d.location === '')
14
+ return
15
+ switch (d.action) {
16
+ case 'REMOVE':
17
+ console.error('REMOVE', d.actual.location)
18
+ break
19
+ case 'ADD':
20
+ console.error('ADD', d.ideal.location, d.ideal.resolved)
21
+ break
22
+ case 'CHANGE':
23
+ console.error('CHANGE', d.actual.location, {
24
+ from: d.actual.resolved,
25
+ to: d.ideal.resolved,
26
+ })
27
+ break
28
+ }
29
+ },
30
+ getChildren: d => d.children,
31
+ })
32
+ }
33
+
34
+ const start = process.hrtime()
35
+ process.emit('time', 'install')
36
+ const arb = new Arborist(options)
37
+ arb.reify(options).then(tree => {
38
+ process.emit('timeEnd', 'install')
39
+ const end = process.hrtime(start)
40
+ print(tree)
41
+ if (options.dryRun)
42
+ printDiff(arb.diff)
43
+ console.error(`resolved ${tree.inventory.size} deps in ${end[0] + end[1] / 1e9}s`)
44
+ if (tree.meta && options.save)
45
+ tree.meta.save()
46
+ }).catch(er => console.error(require('util').inspect(er, { depth: Infinity })))
@@ -0,0 +1,12 @@
1
+ const Shrinkwrap = require('../lib/shrinkwrap.js')
2
+ const options = require('./lib/options.js')
3
+ require('./lib/logging.js')
4
+ require('./lib/timers.js')
5
+
6
+ const { quiet } = options
7
+ Shrinkwrap.load(options)
8
+ .then(s => quiet || console.log(JSON.stringify(s.data, 0, 2)))
9
+ .catch(er => {
10
+ console.error('shrinkwrap load failure', er)
11
+ process.exit(1)
12
+ })
package/bin/virtual.js ADDED
@@ -0,0 +1,15 @@
1
+ const Arborist = require('../')
2
+
3
+ const print = require('./lib/print-tree.js')
4
+ const options = require('./lib/options.js')
5
+ require('./lib/logging.js')
6
+ require('./lib/timers.js')
7
+
8
+ const start = process.hrtime()
9
+ new Arborist(options).loadVirtual().then(tree => {
10
+ const end = process.hrtime(start)
11
+ print(tree)
12
+ if (options.save)
13
+ tree.meta.save()
14
+ console.error(`read ${tree.inventory.size} deps in ${end[0] * 1000 + end[1] / 1e6}ms`)
15
+ }).catch(er => console.error(er))
@@ -397,7 +397,8 @@ module.exports = cls => class IdealTreeBuilder extends cls {
397
397
  // that they're there, and not reinstall the world unnecessarily.
398
398
  if (this[_global] && (this[_updateAll] || this[_updateNames].length)) {
399
399
  const nm = resolve(this.path, 'node_modules')
400
- for (const name of await readdir(nm)) {
400
+ for (const name of await readdir(nm).catch(() => [])) {
401
+ tree.package.dependencies = tree.package.dependencies || {}
401
402
  if (this[_updateAll] || this[_updateNames].includes(name))
402
403
  tree.package.dependencies[name] = '*'
403
404
  }
@@ -416,7 +417,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
416
417
  await this[_add](options)
417
418
 
418
419
  // triggers a refresh of all edgesOut
419
- if (options.add && options.add.length || options.rm && options.rm.length)
420
+ if (options.add && options.add.length || options.rm && options.rm.length || this[_global])
420
421
  tree.package = tree.package
421
422
  process.emit('timeEnd', 'idealTree:userRequests')
422
423
  }
@@ -490,7 +491,8 @@ module.exports = cls => class IdealTreeBuilder extends cls {
490
491
  /* istanbul ignore else - should also be covered by realpath failure */
491
492
  if (filepath) {
492
493
  const { name } = spec
493
- spec = npa(`file:${relpath(this.path, filepath)}`, this.path)
494
+ const tree = this.idealTree.target || this.idealTree
495
+ spec = npa(`file:${relpath(tree.path, filepath)}`, tree.path)
494
496
  spec.name = name
495
497
  }
496
498
  return spec
@@ -662,6 +664,11 @@ This is a one-time fix-up, please be patient...
662
664
  })
663
665
  }
664
666
  await promiseCallLimit(queue)
667
+
668
+ // have to re-calc dep flags, because the nodes don't have edges
669
+ // until their packages get assigned, so everything looks extraneous
670
+ calcDepFlags(this.idealTree)
671
+
665
672
  // yes, yes, this isn't the "original" version, but now that it's been
666
673
  // upgraded, we need to make sure we don't do the work to upgrade it
667
674
  // again, since it's now as new as can be.
@@ -799,6 +806,7 @@ This is a one-time fix-up, please be patient...
799
806
  // a virtual root of whatever brought in THIS node.
800
807
  // so we VR the node itself if the edge is not a peer
801
808
  const source = edge.peer ? peerSource : node
809
+
802
810
  const virtualRoot = this[_virtualRoot](source, true)
803
811
  // reuse virtual root if we already have one, but don't
804
812
  // try to do the override ahead of time, since we MAY be able
@@ -820,13 +828,17 @@ This is a one-time fix-up, please be patient...
820
828
  // +-- z@1
821
829
  // But if x and y are loaded in the same virtual root, then they will
822
830
  // be forced to agree on a version of z.
831
+ const required = new Set([edge.from])
832
+ const parent = edge.peer ? virtualRoot : null
823
833
  const dep = vrDep && vrDep.satisfies(edge) ? vrDep
824
- : await this[_nodeFromEdge](edge, edge.peer ? virtualRoot : null)
834
+ : await this[_nodeFromEdge](edge, parent, null, required)
835
+
825
836
  /* istanbul ignore next */
826
837
  debug(() => {
827
838
  if (!dep)
828
839
  throw new Error('no dep??')
829
840
  })
841
+
830
842
  tasks.push({edge, dep})
831
843
  }
832
844
 
@@ -863,7 +875,7 @@ This is a one-time fix-up, please be patient...
863
875
 
864
876
  // loads a node from an edge, and then loads its peer deps (and their
865
877
  // peer deps, on down the line) into a virtual root parent.
866
- async [_nodeFromEdge] (edge, parent_, secondEdge = null) {
878
+ async [_nodeFromEdge] (edge, parent_, secondEdge, required) {
867
879
  // create a virtual root node with the same deps as the node that
868
880
  // is requesting this one, so that we can get all the peer deps in
869
881
  // a context where they're likely to be resolvable.
@@ -894,6 +906,11 @@ This is a one-time fix-up, please be patient...
894
906
  // ensure the one we want is the one that's placed
895
907
  node.parent = parent
896
908
 
909
+ if (required.has(edge.from) && edge.type !== 'peerOptional' ||
910
+ secondEdge && (
911
+ required.has(secondEdge.from) && secondEdge.type !== 'peerOptional'))
912
+ required.add(node)
913
+
897
914
  // handle otherwise unresolvable dependency nesting loops by
898
915
  // creating a symbolic link
899
916
  // a1 -> b1 -> a2 -> b2 -> a1 -> ...
@@ -907,7 +924,7 @@ This is a one-time fix-up, please be patient...
907
924
  // keep track of the thing that caused this node to be included.
908
925
  const src = parent.sourceReference
909
926
  this[_peerSetSource].set(node, src)
910
- return this[_loadPeerSet](node)
927
+ return this[_loadPeerSet](node, required)
911
928
  }
912
929
 
913
930
  [_virtualRoot] (node, reuse = false) {
@@ -1052,7 +1069,7 @@ This is a one-time fix-up, please be patient...
1052
1069
  // gets placed first. In non-strict mode, we behave strictly if the
1053
1070
  // virtual root is based on the root project, and allow non-peer parent
1054
1071
  // deps to override, but throw if no preference can be determined.
1055
- async [_loadPeerSet] (node) {
1072
+ async [_loadPeerSet] (node, required) {
1056
1073
  const peerEdges = [...node.edgesOut.values()]
1057
1074
  // we typically only install non-optional peers, but we have to
1058
1075
  // factor them into the peerSet so that we can avoid conflicts
@@ -1067,10 +1084,12 @@ This is a one-time fix-up, please be patient...
1067
1084
  const parentEdge = node.parent.edgesOut.get(edge.name)
1068
1085
  const {isProjectRoot, isWorkspace} = node.parent.sourceReference
1069
1086
  const isMine = isProjectRoot || isWorkspace
1087
+ const conflictOK = this[_force] || !isMine && !this[_strictPeerDeps]
1088
+
1070
1089
  if (!edge.to) {
1071
1090
  if (!parentEdge) {
1072
1091
  // easy, just put the thing there
1073
- await this[_nodeFromEdge](edge, node.parent)
1092
+ await this[_nodeFromEdge](edge, node.parent, null, required)
1074
1093
  continue
1075
1094
  } else {
1076
1095
  // if the parent's edge is very broad like >=1, and the edge in
@@ -1081,14 +1100,16 @@ This is a one-time fix-up, please be patient...
1081
1100
  // a conflict. this is always a problem in strict mode, never
1082
1101
  // in force mode, and a problem in non-strict mode if this isn't
1083
1102
  // on behalf of our project. in all such cases, we warn at least.
1084
- await this[_nodeFromEdge](parentEdge, node.parent, edge)
1103
+ const dep = await this[_nodeFromEdge](parentEdge, node.parent, edge, required)
1085
1104
 
1086
1105
  // hooray! that worked!
1087
1106
  if (edge.valid)
1088
1107
  continue
1089
1108
 
1090
- // allow it
1091
- if (this[_force] || !isMine && !this[_strictPeerDeps])
1109
+ // allow it. either we're overriding, or it's not something
1110
+ // that will be installed by default anyway, and we'll fail when
1111
+ // we get to the point where we need to, if we need to.
1112
+ if (conflictOK || !required.has(dep))
1092
1113
  continue
1093
1114
 
1094
1115
  // problem
@@ -1101,7 +1122,7 @@ This is a one-time fix-up, please be patient...
1101
1122
  // in non-strict mode if it's not our fault. don't warn here, because
1102
1123
  // we are going to warn again when we place the deps, if we end up
1103
1124
  // overriding for something else.
1104
- if (this[_force] || !isMine && !this[_strictPeerDeps])
1125
+ if (conflictOK)
1105
1126
  continue
1106
1127
 
1107
1128
  // ok, it's the root, or we're in unforced strict mode, so this is bad
@@ -1148,6 +1169,7 @@ This is a one-time fix-up, please be patient...
1148
1169
  [_placeDep] (dep, node, edge, peerEntryEdge = null, peerPath = []) {
1149
1170
  if (edge.to &&
1150
1171
  !edge.error &&
1172
+ !this[_explicitRequests].has(edge.name) &&
1151
1173
  !this[_updateNames].includes(edge.name) &&
1152
1174
  !this[_isVulnerable](edge.to))
1153
1175
  return []
@@ -1196,8 +1218,25 @@ This is a one-time fix-up, please be patient...
1196
1218
  break
1197
1219
  }
1198
1220
 
1199
- if (!target)
1200
- this[_failPeerConflict](edge)
1221
+ // if we can't find a target, that means that the last placed checked
1222
+ // (and all the places before it) had a copy already. if we're in
1223
+ // --force mode, then the user has explicitly said that they're ok
1224
+ // with conflicts. This can only occur in --force mode in the case
1225
+ // when a node was added to the tree with a peerOptional dep that we
1226
+ // ignored, and then later, that edge became invalid, and we fail to
1227
+ // resolve it. We will warn about it in a moment.
1228
+ if (!target) {
1229
+ if (this[_force]) {
1230
+ // we know that there is a dep (not the root) which is the target
1231
+ // of this edge, or else it wouldn't have been a conflict.
1232
+ target = edge.to.resolveParent
1233
+ canPlace = KEEP
1234
+ } else
1235
+ this[_failPeerConflict](edge)
1236
+ } else {
1237
+ // it worked, so we clearly have no peer conflicts at this point.
1238
+ this[_peerConflict] = null
1239
+ }
1201
1240
 
1202
1241
  this.log.silly(
1203
1242
  'placeDep',
@@ -1208,9 +1247,6 @@ This is a one-time fix-up, please be patient...
1208
1247
  `want: ${edge.spec || '*'}`
1209
1248
  )
1210
1249
 
1211
- // it worked, so we clearly have no peer conflicts at this point.
1212
- this[_peerConflict] = null
1213
-
1214
1250
  // Can only get KEEP here if the original edge was valid,
1215
1251
  // and we're checking for an update but it's already up to date.
1216
1252
  if (canPlace === KEEP) {
@@ -1396,6 +1432,7 @@ This is a one-time fix-up, please be patient...
1396
1432
  })
1397
1433
  const entryEdge = peerEntryEdge || edge
1398
1434
  const source = this[_peerSetSource].get(dep)
1435
+
1399
1436
  isSource = isSource || target === source
1400
1437
  // if we're overriding the source, then we care if the *target* is
1401
1438
  // ours, even if it wasn't actually the original source, since we
@@ -24,6 +24,7 @@ const loadWorkspacesVirtual = Symbol.for('loadWorkspacesVirtual')
24
24
  const flagsSuspect = Symbol.for('flagsSuspect')
25
25
  const reCalcDepFlags = Symbol('reCalcDepFlags')
26
26
  const checkRootEdges = Symbol('checkRootEdges')
27
+ const rootOptionProvided = Symbol('rootOptionProvided')
27
28
 
28
29
  const depsToEdges = (type, deps) =>
29
30
  Object.entries(deps).map(d => [type, ...d])
@@ -63,6 +64,8 @@ module.exports = cls => class VirtualLoader extends cls {
63
64
  root = await this[loadRoot](s),
64
65
  } = options
65
66
 
67
+ this[rootOptionProvided] = options.root
68
+
66
69
  await this[loadFromShrinkwrap](s, root)
67
70
  return treeCheck(this.virtualTree)
68
71
  }
@@ -74,13 +77,17 @@ module.exports = cls => class VirtualLoader extends cls {
74
77
  }
75
78
 
76
79
  async [loadFromShrinkwrap] (s, root) {
77
- // root is never any of these things, but might be a brand new
78
- // baby Node object that never had its dep flags calculated.
79
- root.extraneous = false
80
- root.dev = false
81
- root.optional = false
82
- root.devOptional = false
83
- root.peer = false
80
+ if (!this[rootOptionProvided]) {
81
+ // root is never any of these things, but might be a brand new
82
+ // baby Node object that never had its dep flags calculated.
83
+ root.extraneous = false
84
+ root.dev = false
85
+ root.optional = false
86
+ root.devOptional = false
87
+ root.peer = false
88
+ } else
89
+ this[flagsSuspect] = true
90
+
84
91
  this[checkRootEdges](s, root)
85
92
  root.meta = s
86
93
  this.virtualTree = root
@@ -88,20 +95,23 @@ module.exports = cls => class VirtualLoader extends cls {
88
95
  await this[resolveLinks](links, nodes)
89
96
  this[assignBundles](nodes)
90
97
  if (this[flagsSuspect])
91
- this[reCalcDepFlags]()
98
+ this[reCalcDepFlags](nodes.values())
92
99
  return root
93
100
  }
94
101
 
95
- [reCalcDepFlags] () {
102
+ [reCalcDepFlags] (nodes) {
96
103
  // reset all dep flags
97
- for (const node of this.virtualTree.inventory.values()) {
104
+ // can't use inventory here, because virtualTree might not be root
105
+ for (const node of nodes) {
106
+ if (node.isRoot || node === this[rootOptionProvided])
107
+ continue
98
108
  node.extraneous = true
99
109
  node.dev = true
100
110
  node.optional = true
101
111
  node.devOptional = true
102
112
  node.peer = true
103
113
  }
104
- calcDepFlags(this.virtualTree, true)
114
+ calcDepFlags(this.virtualTree, !this[rootOptionProvided])
105
115
  }
106
116
 
107
117
  // check the lockfile deps, and see if they match. if they do not
@@ -237,6 +247,12 @@ module.exports = cls => class VirtualLoader extends cls {
237
247
  // shrinkwrap doesn't include package name unless necessary
238
248
  if (!sw.name)
239
249
  sw.name = nameFromFolder(path)
250
+
251
+ const dev = sw.dev
252
+ const optional = sw.optional
253
+ const devOptional = dev || optional || sw.devOptional
254
+ const peer = sw.peer
255
+
240
256
  const node = new Node({
241
257
  legacyPeerDeps: this.legacyPeerDeps,
242
258
  root: this.virtualTree,
@@ -246,6 +262,10 @@ module.exports = cls => class VirtualLoader extends cls {
246
262
  resolved: consistentResolve(sw.resolved, this.path, path),
247
263
  pkg: sw,
248
264
  hasShrinkwrap: sw.hasShrinkwrap,
265
+ dev,
266
+ optional,
267
+ devOptional,
268
+ peer,
249
269
  })
250
270
  // cast to boolean because they're undefined in the lock file when false
251
271
  node.extraneous = !!sw.extraneous
@@ -233,7 +233,7 @@ module.exports = cls => class Reifier extends cls {
233
233
  const actualOpt = this[_global] ? {
234
234
  ignoreMissing: true,
235
235
  global: true,
236
- filter: (node, kid) => !node.isRoot && node !== node.root.target
236
+ filter: (node, kid) => this[_explicitRequests].size === 0 || !node.isProjectRoot
237
237
  ? true
238
238
  : (node.edgesOut.has(kid) || this[_explicitRequests].has(kid)),
239
239
  } : { ignoreMissing: true }
@@ -442,7 +442,8 @@ module.exports = cls => class Reifier extends cls {
442
442
  if (this[_trashList].has(node.path))
443
443
  return node
444
444
 
445
- process.emit('time', `reifyNode:${node.location}`)
445
+ const timer = `reifyNode:${node.location}`
446
+ process.emit('time', timer)
446
447
  this.addTracker('reify', node.name, node.location)
447
448
 
448
449
  const p = Promise.resolve()
@@ -454,7 +455,7 @@ module.exports = cls => class Reifier extends cls {
454
455
  return this[_handleOptionalFailure](node, p)
455
456
  .then(() => {
456
457
  this.finishTracker('reify', node.name, node.location)
457
- process.emit('timeEnd', `reifyNode:${node.location}`)
458
+ process.emit('timeEnd', timer)
458
459
  return node
459
460
  })
460
461
  }
@@ -474,9 +475,14 @@ module.exports = cls => class Reifier extends cls {
474
475
 
475
476
  // no idea what this thing is. remove it from the tree.
476
477
  if (!res) {
477
- node.parent = null
478
+ const warning = 'invalid or damaged lockfile detected\n' +
479
+ 'please re-try this operation once it completes\n' +
480
+ 'so that the damage can be corrected, or perform\n' +
481
+ 'a fresh install with no lockfile if the problem persists.'
482
+ this.log.warn('reify', warning)
478
483
  this.log.verbose('reify', 'unrecognized node in tree', node.path)
479
484
  node.parent = null
485
+ node.fsParent = null
480
486
  this[_addNodeToTrashList](node)
481
487
  return
482
488
  }
@@ -605,7 +611,7 @@ module.exports = cls => class Reifier extends cls {
605
611
  tree: this.diff,
606
612
  visit: diff => {
607
613
  const node = diff.ideal
608
- if (node && !node.isRoot && node.package.bundleDependencies &&
614
+ if (node && !node.isProjectRoot && node.package.bundleDependencies &&
609
615
  node.package.bundleDependencies.length) {
610
616
  maxBundleDepth = Math.max(maxBundleDepth, node.depth)
611
617
  if (!bundlesByDepth.has(node.depth))
@@ -811,7 +817,7 @@ module.exports = cls => class Reifier extends cls {
811
817
  dfwalk({
812
818
  tree: this.diff,
813
819
  leave: diff => {
814
- if (!diff.ideal.isRoot)
820
+ if (!diff.ideal.isProjectRoot)
815
821
  nodes.push(diff.ideal)
816
822
  },
817
823
  // process adds before changes, ignore removals
@@ -907,7 +913,7 @@ module.exports = cls => class Reifier extends cls {
907
913
 
908
914
  return Promise.all([
909
915
  this[_saveLockFile](saveOpt),
910
- updateRootPackageJson({ tree: this.idealTree }),
916
+ updateRootPackageJson(this.idealTree),
911
917
  ]).then(() => process.emit('timeEnd', 'reify:save'))
912
918
  }
913
919
 
@@ -11,7 +11,7 @@ const calcDepFlags = (tree, resetRoot = true) => {
11
11
  tree,
12
12
  visit: node => calcDepFlagsStep(node),
13
13
  filter: node => node,
14
- getChildren: node => [...node.edgesOut.values()].map(edge => edge.to),
14
+ getChildren: (node, tree) => [...tree.edgesOut.values()].map(edge => edge.to),
15
15
  })
16
16
  return ret
17
17
  }
package/lib/node.js CHANGED
@@ -247,7 +247,7 @@ class Node {
247
247
 
248
248
  // true for packages installed directly in the global node_modules folder
249
249
  get globalTop () {
250
- return this.global && this.parent.isRoot
250
+ return this.global && this.parent.isProjectRoot
251
251
  }
252
252
 
253
253
  get workspaces () {
@@ -294,8 +294,11 @@ class Node {
294
294
  const { name = '', version = '' } = this.package
295
295
  // root package will prefer package name over folder name,
296
296
  // and never be called an alias.
297
- const myname = this.isRoot ? name || this.name : this.name
298
- const alias = !this.isRoot && name && myname !== name ? `npm:${name}@` : ''
297
+ const { isProjectRoot } = this
298
+ const myname = isProjectRoot ? name || this.name
299
+ : this.name
300
+ const alias = !isProjectRoot && name && myname !== name ? `npm:${name}@`
301
+ : ''
299
302
  return `${myname}@${alias}${version}`
300
303
  }
301
304
 
@@ -339,14 +342,14 @@ class Node {
339
342
  }
340
343
 
341
344
  [_explain] (edge, seen) {
342
- if (this.isRoot && !this.sourceReference) {
345
+ if (this.isProjectRoot && !this.sourceReference) {
343
346
  return {
344
347
  location: this.path,
345
348
  }
346
349
  }
347
350
 
348
351
  const why = {
349
- name: this.isRoot ? this.package.name : this.name,
352
+ name: this.isProjectRoot ? this.package.name : this.name,
350
353
  version: this.package.version,
351
354
  }
352
355
  if (this.errors.length || !this.package.name || !this.package.version) {
@@ -384,7 +387,7 @@ class Node {
384
387
  // and are not keeping it held in this spot anyway.
385
388
  const edges = []
386
389
  for (const edge of this.edgesIn) {
387
- if (!edge.valid && !edge.from.isRoot)
390
+ if (!edge.valid && !edge.from.isProjectRoot)
388
391
  continue
389
392
 
390
393
  edges.push(edge)
@@ -453,7 +456,7 @@ class Node {
453
456
  }
454
457
 
455
458
  get isWorkspace () {
456
- if (this.isRoot)
459
+ if (this.isProjectRoot)
457
460
  return false
458
461
  const { root } = this
459
462
  const { type, to } = root.edgesOut.get(this.package.name) || {}
@@ -733,7 +736,9 @@ class Node {
733
736
  // Linked targets that are disconnected from the tree are tops,
734
737
  // but don't have a 'path' field, only a 'realpath', because we
735
738
  // don't know their canonical location. We don't need their devDeps.
736
- if (this.isTop && this.path && !this.sourceReference)
739
+ const { isTop, path, sourceReference } = this
740
+ const { isTop: srcTop, path: srcPath } = sourceReference || {}
741
+ if (isTop && path && (!sourceReference || srcTop && srcPath))
737
742
  this[_loadDepType](this.package.devDependencies, 'dev')
738
743
 
739
744
  const pd = this.package.peerDependencies
@@ -902,8 +907,8 @@ class Node {
902
907
  if (this.isLink)
903
908
  return node.isLink && this.target.matches(node.target)
904
909
 
905
- // if they're two root nodes, they're different if the paths differ
906
- if (this.isRoot && node.isRoot)
910
+ // if they're two project root nodes, they're different if the paths differ
911
+ if (this.isProjectRoot && node.isProjectRoot)
907
912
  return this.path === node.path
908
913
 
909
914
  // if the integrity matches, then they're the same.
@@ -15,7 +15,7 @@ const depTypes = new Set([
15
15
  'peerDependencies',
16
16
  ])
17
17
 
18
- async function updateRootPackageJson ({ tree }) {
18
+ const updateRootPackageJson = async tree => {
19
19
  const filename = resolve(tree.path, 'package.json')
20
20
  const originalContent = await readFile(filename, 'utf8')
21
21
  .then(data => parseJSON(data))
@@ -25,6 +25,16 @@ async function updateRootPackageJson ({ tree }) {
25
25
  ...tree.package,
26
26
  })
27
27
 
28
+ // optionalDependencies don't need to be repeated in two places
29
+ if (depsData.dependencies) {
30
+ if (depsData.optionalDependencies) {
31
+ for (const name of Object.keys(depsData.optionalDependencies))
32
+ delete depsData.dependencies[name]
33
+ }
34
+ if (Object.keys(depsData.dependencies).length === 0)
35
+ delete depsData.dependencies
36
+ }
37
+
28
38
  // if there's no package.json, just use internal pkg info as source of truth
29
39
  const packageJsonContent = originalContent || depsData
30
40
 
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "2.1.0",
3
+ "version": "2.2.2",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
- "@npmcli/installed-package-contents": "^1.0.5",
7
- "@npmcli/map-workspaces": "^1.0.1",
6
+ "@npmcli/installed-package-contents": "^1.0.6",
7
+ "@npmcli/map-workspaces": "^1.0.2",
8
8
  "@npmcli/metavuln-calculator": "^1.0.1",
9
9
  "@npmcli/move-file": "^1.1.0",
10
10
  "@npmcli/name-from-folder": "^1.0.1",
11
11
  "@npmcli/node-gyp": "^1.0.1",
12
- "@npmcli/run-script": "^1.8.1",
12
+ "@npmcli/run-script": "^1.8.2",
13
13
  "bin-links": "^2.2.1",
14
14
  "cacache": "^15.0.3",
15
15
  "common-ancestor-path": "^1.0.1",
@@ -20,11 +20,11 @@
20
20
  "npm-package-arg": "^8.1.0",
21
21
  "npm-pick-manifest": "^6.1.0",
22
22
  "npm-registry-fetch": "^9.0.0",
23
- "pacote": "^11.2.3",
23
+ "pacote": "^11.2.6",
24
24
  "parse-conflict-json": "^1.1.1",
25
25
  "promise-all-reject-late": "^1.0.0",
26
26
  "promise-call-limit": "^1.0.1",
27
- "read-package-json-fast": "^1.2.1",
27
+ "read-package-json-fast": "^2.0.1",
28
28
  "readdir-scoped-modules": "^1.1.0",
29
29
  "semver": "^7.3.4",
30
30
  "tar": "^6.1.0",
@@ -55,7 +55,7 @@
55
55
  "postversion": "npm publish",
56
56
  "prepublishOnly": "git push origin --follow-tags",
57
57
  "eslint": "eslint",
58
- "lint": "npm run eslint -- \"lib/**/*.js\"",
58
+ "lint": "npm run eslint -- \"lib/**/*.js\" \"test/arborist/*.js\" \"test/*.js\" \"bin/**/*.js\"",
59
59
  "lintfix": "npm run lint -- --fix",
60
60
  "benchmark": "node scripts/benchmark.js",
61
61
  "benchclean": "rm -rf scripts/benchmark/*/"
@@ -67,9 +67,13 @@
67
67
  "author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
68
68
  "license": "ISC",
69
69
  "files": [
70
- "lib/**/*.js"
70
+ "lib/**/*.js",
71
+ "bin/**/*.js"
71
72
  ],
72
73
  "main": "lib/index.js",
74
+ "bin": {
75
+ "arborist": "bin/index.js"
76
+ },
73
77
  "tap": {
74
78
  "100": true,
75
79
  "node-arg": [