Skip to content

Commit d438d61

Browse files
committedFeb 24, 2022
feat(arborist): refactor arborist bin to use consistent timing/logging
This attempts to make the arborist bin script behave more like the npm cli with regards to the handing of timing and logging. It also adds the a `logfile` argument to write logs to a file instead of (or in addition to) stderr. This can be helpful for benchmarking performance of loggins or terminal display.
1 parent ce1c2bf commit d438d61

17 files changed

+490
-359
lines changed
 

‎package-lock.json

+2
Original file line numberDiff line numberDiff line change
@@ -10434,6 +10434,7 @@
1043410434
"json-stringify-nice": "^1.1.4",
1043510435
"mkdirp": "^1.0.4",
1043610436
"mkdirp-infer-owner": "^2.0.0",
10437+
"nopt": "^5.0.0",
1043710438
"npm-install-checks": "^4.0.0",
1043810439
"npm-package-arg": "^9.0.0",
1043910440
"npm-pick-manifest": "^7.0.0",
@@ -11353,6 +11354,7 @@
1135311354
"mkdirp": "^1.0.4",
1135411355
"mkdirp-infer-owner": "^2.0.0",
1135511356
"nock": "^13.2.0",
11357+
"nopt": "^5.0.0",
1135611358
"npm-install-checks": "^4.0.0",
1135711359
"npm-package-arg": "^9.0.0",
1135811360
"npm-pick-manifest": "^7.0.0",

‎workspaces/arborist/README.md

+10
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,13 @@ pruning nodes from the tree.
333333

334334
Note: `devOptional` is only set in the shrinkwrap/package-lock file if
335335
_neither_ `dev` nor `optional` are set, as it would be redundant.
336+
337+
## BIN
338+
339+
Arborist ships with a cli that can be used to run arborist specific commands outside of the context of the npm CLI. This script is currently not part of the public API and is subject to breaking changes outside of major version bumps.
340+
341+
To see the usage run:
342+
343+
```
344+
npx @npmcli/arborist --help
345+
```

‎workspaces/arborist/bin/actual.js

+16-20
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
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')
62

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-
}
3+
const printTree = require('./lib/print-tree.js')
134

14-
console.error(`read ${tree.inventory.size} deps in ${end[0] * 1000 + end[1] / 1e6}ms`)
15-
if (options.save) {
16-
tree.meta.save()
17-
}
18-
if (options.saveHidden) {
19-
tree.meta.hiddenLockfile = true
20-
tree.meta.filename = options.path + '/node_modules/.package-lock.json'
21-
tree.meta.save()
22-
}
23-
}).catch(er => console.error(er))
5+
module.exports = (options, time) => new Arborist(options)
6+
.loadActual(options)
7+
.then(time)
8+
.then(async ({ timing, result: tree }) => {
9+
printTree(tree)
10+
if (options.save) {
11+
await tree.meta.save()
12+
}
13+
if (options.saveHidden) {
14+
tree.meta.hiddenLockfile = true
15+
tree.meta.filename = options.path + '/node_modules/.package-lock.json'
16+
await tree.meta.save()
17+
}
18+
return `read ${tree.inventory.size} deps in ${timing.ms}`
19+
})

‎workspaces/arborist/bin/audit.js

+23-26
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
const Arborist = require('../')
22

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')
3+
const printTree = require('./lib/print-tree.js')
4+
const log = require('./lib/logging.js')
75

86
const Vuln = require('../lib/vuln.js')
97
const printReport = report => {
108
for (const vuln of report.values()) {
11-
console.log(printVuln(vuln))
9+
log.info(printVuln(vuln))
1210
}
1311
if (report.topVulns.size) {
14-
console.log('\n# top-level vulnerabilities')
12+
log.info('\n# top-level vulnerabilities')
1513
for (const vuln of report.topVulns.values()) {
16-
console.log(printVuln(vuln))
14+
log.info(printVuln(vuln))
1715
}
1816
}
1917
}
@@ -33,22 +31,21 @@ const printVuln = vuln => {
3331

3432
const printAdvisory = a => `${a.title}${a.url ? ' ' + a.url : ''}`
3533

36-
const start = process.hrtime()
37-
process.emit('time', 'audit script')
38-
const arb = new Arborist(options)
39-
arb.audit(options).then(tree => {
40-
process.emit('timeEnd', 'audit script')
41-
const end = process.hrtime(start)
42-
if (options.fix) {
43-
print(tree)
44-
}
45-
if (!options.quiet) {
46-
printReport(arb.auditReport)
47-
}
48-
if (options.fix) {
49-
console.error(`resolved ${tree.inventory.size} deps in ${end[0] + end[1] / 1e9}s`)
50-
}
51-
if (tree.meta && options.save) {
52-
tree.meta.save()
53-
}
54-
}).catch(er => console.error(er))
34+
module.exports = (options, time) => {
35+
const arb = new Arborist(options)
36+
return arb
37+
.audit(options)
38+
.then(time)
39+
.then(async ({ timing, result: tree }) => {
40+
if (options.fix) {
41+
printTree(tree)
42+
}
43+
printReport(arb.auditReport)
44+
if (tree.meta && options.save) {
45+
await tree.meta.save()
46+
}
47+
return options.fix
48+
? `resolved ${tree.inventory.size} deps in ${timing.seconds}`
49+
: `done in ${timing.seconds}`
50+
})
51+
}

‎workspaces/arborist/bin/funding.js

+35-31
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,38 @@
1-
const options = require('./lib/options.js')
2-
require('./lib/logging.js')
3-
require('./lib/timers.js')
4-
51
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)
2+
3+
const log = require('./lib/logging.js')
4+
5+
module.exports = (options, time) => {
6+
const query = options._.shift()
7+
const a = new Arborist(options)
8+
return a
9+
.loadVirtual()
10+
.then(tree => {
11+
// only load the actual tree if the virtual one doesn't have modern metadata
12+
if (!tree.meta || !(tree.meta.originalLockfileVersion >= 2)) {
13+
log.error('old metadata, load actual')
14+
throw 'load actual'
15+
} else {
16+
log.error('meta ok, return virtual tree')
17+
return tree
2418
}
25-
}
26-
} else {
27-
for (const node of tree.inventory.query('name', query)) {
28-
if (node.package.funding) {
29-
console.log(node.name, node.location, node.package.funding)
19+
})
20+
.catch(() => a.loadActual())
21+
.then(time)
22+
.then(({ timing, result: tree }) => {
23+
if (!query) {
24+
for (const node of tree.inventory.values()) {
25+
if (node.package.funding) {
26+
log.info(node.name, node.location, node.package.funding)
27+
}
28+
}
29+
} else {
30+
for (const node of tree.inventory.query('name', query)) {
31+
if (node.package.funding) {
32+
log.info(node.name, node.location, node.package.funding)
33+
}
34+
}
3035
}
31-
}
32-
}
33-
console.error(`read ${tree.inventory.size} deps in ${end[0] * 1000 + end[1] / 1e6}ms`)
34-
})
36+
return `read ${tree.inventory.size} deps in ${timing.ms}`
37+
})
38+
}

‎workspaces/arborist/bin/ideal.js

+11-18
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
11
const Arborist = require('../')
22

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')
3+
const printTree = require('./lib/print-tree.js')
84

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-
}
17-
}).catch(er => {
18-
const opt = { depth: Infinity, color: true }
19-
console.error(er.code === 'ERESOLVE' ? inspect(er, opt) : er)
20-
process.exitCode = 1
21-
})
5+
module.exports = (options, time) => new Arborist(options)
6+
.buildIdealTree(options)
7+
.then(time)
8+
.then(async ({ timing, result: tree }) => {
9+
printTree(tree)
10+
if (tree.meta && options.save) {
11+
await tree.meta.save()
12+
}
13+
return `resolved ${tree.inventory.size} deps in ${timing.seconds}`
14+
})

‎workspaces/arborist/bin/index.js

+92-63
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,110 @@
11
#!/usr/bin/env node
2-
const [cmd] = process.argv.splice(2, 1)
32

4-
const usage = () => `Arborist - the npm tree doctor
3+
const fs = require('fs')
4+
const path = require('path')
55

6-
Version: ${require('../package.json').version}
6+
const { bin, arb: options } = require('./lib/options')
7+
const version = require('../package.json').version
78

9+
const usage = (message = '') => `Arborist - the npm tree doctor
10+
11+
Version: ${version}
12+
${message && '\n' + message + '\n'}
813
# USAGE
914
arborist <cmd> [path] [options...]
1015
1116
# COMMANDS
1217
13-
* reify: reify ideal tree to node_modules (install, update, rm, ...)
14-
* prune: prune the ideal tree and reify (like npm prune)
15-
* ideal: generate and print the ideal tree
16-
* actual: read and print the actual tree in node_modules
17-
* virtual: read and print the virtual tree in the local shrinkwrap file
18-
* shrinkwrap: load a local shrinkwrap and print its data
19-
* audit: perform a security audit on project dependencies
20-
* funding: query funding information in the local package tree. A second
21-
positional argument after the path name can limit to a package name.
22-
* license: query license information in the local package tree. A second
23-
positional argument after the path name can limit to a license type.
24-
* help: print this text
18+
* reify: reify ideal tree to node_modules (install, update, rm, ...)
19+
* prune: prune the ideal tree and reify (like npm prune)
20+
* ideal: generate and print the ideal tree
21+
* actual: read and print the actual tree in node_modules
22+
* virtual: read and print the virtual tree in the local shrinkwrap file
23+
* shrinkwrap: load a local shrinkwrap and print its data
24+
* audit: perform a security audit on project dependencies
25+
* funding: query funding information in the local package tree. A second
26+
positional argument after the path name can limit to a package name.
27+
* license: query license information in the local package tree. A second
28+
positional argument after the path name can limit to a license type.
29+
* help: print this text
30+
* version: print the version
2531
2632
# OPTIONS
2733
28-
Most npm options are supported, but in camelCase rather than css-case. For
29-
example, instead of '--dry-run', use '--dryRun'.
34+
Most npm options are supported, but in camelCase rather than css-case. For
35+
example, instead of '--dry-run', use '--dryRun'.
3036
31-
Additionally:
37+
Additionally:
3238
33-
* --quiet will supppress the printing of package trees
34-
* Instead of 'npm install <pkg>', use 'arborist reify --add=<pkg>'.
35-
The '--add=<pkg>' option can be specified multiple times.
36-
* Instead of 'npm rm <pkg>', use 'arborist reify --rm=<pkg>'.
37-
The '--rm=<pkg>' option can be specified multiple times.
38-
* Instead of 'npm update', use 'arborist reify --update-all'.
39-
* 'npm audit fix' is 'arborist audit --fix'
39+
* --loglevel=warn|--quiet will supppress the printing of package trees
40+
* --logfile <file|bool> will output logs to a file
41+
* --timing will show timing information
42+
* Instead of 'npm install <pkg>', use 'arborist reify --add=<pkg>'.
43+
The '--add=<pkg>' option can be specified multiple times.
44+
* Instead of 'npm rm <pkg>', use 'arborist reify --rm=<pkg>'.
45+
The '--rm=<pkg>' option can be specified multiple times.
46+
* Instead of 'npm update', use 'arborist reify --update-all'.
47+
* 'npm audit fix' is 'arborist audit --fix'
4048
`
4149

42-
const help = () => console.log(usage())
43-
44-
switch (cmd) {
45-
case 'actual':
46-
require('./actual.js')
47-
break
48-
case 'virtual':
49-
require('./virtual.js')
50-
break
51-
case 'ideal':
52-
require('./ideal.js')
53-
break
54-
case 'prune':
55-
require('./prune.js')
56-
break
57-
case 'reify':
58-
require('./reify.js')
59-
break
60-
case 'audit':
61-
require('./audit.js')
62-
break
63-
case 'funding':
64-
require('./funding.js')
65-
break
66-
case 'license':
67-
require('./license.js')
68-
break
69-
case 'shrinkwrap':
70-
require('./shrinkwrap.js')
71-
break
72-
case 'help':
73-
case '-h':
74-
case '--help':
75-
help()
76-
break
77-
default:
50+
const commands = {
51+
version: () => console.log(version),
52+
help: () => console.log(usage()),
53+
exit: () => {
7854
process.exitCode = 1
79-
console.error(usage())
80-
break
55+
console.error(
56+
usage(`Error: command '${bin.command}' does not exist.`)
57+
)
58+
},
59+
}
60+
61+
const commandFiles = fs.readdirSync(__dirname).filter((f) => path.extname(f) === '.js' && f !== __filename)
62+
63+
for (const file of commandFiles) {
64+
const command = require(`./${file}`)
65+
const name = path.basename(file, '.js')
66+
const totalTime = `bin:${name}:init`
67+
const scriptTime = `bin:${name}:script`
68+
69+
commands[name] = () => {
70+
const timers = require('./lib/timers')
71+
const log = require('./lib/logging')
72+
73+
log.info(name, options)
74+
75+
process.emit('time', totalTime)
76+
process.emit('time', scriptTime)
77+
78+
return command(options, (result) => {
79+
process.emit('timeEnd', scriptTime)
80+
return {
81+
result,
82+
timing: {
83+
seconds: `${timers.get(scriptTime) / 1e9}s`,
84+
ms: `${timers.get(scriptTime) / 1e6}ms`,
85+
},
86+
}
87+
})
88+
.then((result) => {
89+
log.info(result)
90+
return result
91+
})
92+
.catch((err) => {
93+
process.exitCode = 1
94+
log.error(err)
95+
return err
96+
})
97+
.then((r) => {
98+
process.emit('timeEnd', totalTime)
99+
if (bin.loglevel !== 'silent') {
100+
console[process.exitCode ? 'error' : 'log'](r)
101+
}
102+
})
103+
}
104+
}
105+
106+
if (commands[bin.command]) {
107+
commands[bin.command]()
108+
} else {
109+
commands.exit()
81110
}
+62-26
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,78 @@
1-
const options = require('./options.js')
2-
const { quiet = false } = options
3-
const { loglevel = quiet ? 'warn' : 'silly' } = options
1+
const log = require('proc-log')
2+
const mkdirp = require('mkdirp')
3+
const fs = require('fs')
4+
const { dirname } = require('path')
5+
const os = require('os')
6+
const { inspect, format } = require('util')
7+
8+
const { bin: options } = require('./options.js')
49

5-
const levels = [
10+
// add a meta method to proc-log for passing optional
11+
// metadata through to log handlers
12+
const META = Symbol('meta')
13+
const parseArgs = (...args) => {
14+
const { [META]: isMeta } = args[args.length - 1] || {}
15+
return isMeta
16+
? [args[args.length - 1], ...args.slice(0, args.length - 1)]
17+
: [{}, ...args]
18+
}
19+
log.meta = (meta = {}) => ({ [META]: true, ...meta })
20+
21+
const levels = new Map([
622
'silly',
723
'verbose',
824
'info',
9-
'timing',
1025
'http',
1126
'notice',
1227
'warn',
1328
'error',
1429
'silent',
15-
]
30+
].map((level, index) => [level, index]))
1631

17-
const levelMap = new Map(levels.reduce((set, level, index) => {
18-
set.push([level, index], [index, level])
19-
return set
20-
}, []))
32+
const addLogListener = (write, { eol = os.EOL, loglevel = 'silly', colors = false } = {}) => {
33+
const levelIndex = levels.get(loglevel)
2134

22-
const { inspect, format } = require('util')
23-
const colors = process.stderr.isTTY
24-
const magenta = colors ? msg => `\x1B[35m${msg}\x1B[39m` : m => m
25-
if (loglevel !== 'silent') {
26-
process.on('log', (level, ...args) => {
27-
if (levelMap.get(level) < levelMap.get(loglevel)) {
28-
return
35+
const magenta = m => colors ? `\x1B[35m${m}\x1B[39m` : m
36+
const dim = m => colors ? `\x1B[2m${m}\x1B[22m` : m
37+
const red = m => colors ? `\x1B[31m${m}\x1B[39m` : m
38+
39+
const formatter = (level, ...args) => {
40+
const depth = level === 'error' && args[0] && args[0].code === 'ERESOLVE' ? Infinity : 10
41+
42+
if (level === 'info' && args[0] === 'timeEnd') {
43+
args[1] = dim(args[1])
44+
} else if (level === 'error' && args[0] === 'timeError') {
45+
args[1] = red(args[1])
2946
}
47+
48+
const messages = args.map(a => typeof a === 'string' ? a : inspect(a, { depth, colors }))
3049
const pref = `${process.pid} ${magenta(level)} `
31-
if (level === 'warn' && args[0] === 'ERESOLVE') {
32-
args[2] = inspect(args[2], { depth: 10, colors })
33-
} else {
34-
args = args.map(a => {
35-
return typeof a === 'string' ? a
36-
: inspect(a, { depth: 10, colors })
37-
})
50+
51+
return pref + format(...messages).trim().split('\n').join(`${eol}${pref}`) + eol
52+
}
53+
54+
process.on('log', (...args) => {
55+
const [meta, level, ...logArgs] = parseArgs(...args)
56+
57+
if (levelIndex <= levels.get(level) || meta.force) {
58+
write(formatter(level, ...logArgs))
3859
}
39-
const msg = pref + format(...args).trim().split('\n').join(`\n${pref}`)
40-
console.error(msg)
4160
})
4261
}
62+
63+
if (options.loglevel !== 'silent') {
64+
addLogListener((v) => process.stderr.write(v), {
65+
eol: '\n',
66+
colors: options.colors,
67+
loglevel: options.loglevel,
68+
})
69+
}
70+
71+
if (options.logfile) {
72+
log.silly('logfile', options.logfile)
73+
mkdirp.sync(dirname(options.logfile))
74+
const fd = fs.openSync(options.logfile, 'a')
75+
addLogListener((str) => fs.writeSync(fd, str))
76+
}
77+
78+
module.exports = log
+117-53
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,123 @@
1-
const options = module.exports = {
2-
path: undefined,
3-
cache: `${process.env.HOME}/.npm/_cacache`,
4-
_: [],
1+
const nopt = require('nopt')
2+
const path = require('path')
3+
4+
const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
5+
6+
const cleanPath = (val) => {
7+
const k = Symbol('key')
8+
const data = {}
9+
nopt.typeDefs.path.validate(data, k, val)
10+
return data[k]
511
}
612

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 (/^--before=/.test(arg)) {
35-
options.before = new Date(arg.substr('--before='.length))
36-
} else if (/^-w.+/.test(arg)) {
37-
options.workspaces = options.workspaces || []
38-
options.workspaces.push(arg.replace(/^-w/, ''))
39-
} else if (/^--workspace=/.test(arg)) {
40-
options.workspaces = options.workspaces || []
41-
options.workspaces.push(arg.replace(/^--workspace=/, ''))
42-
} else if (/^--[^=]+=/.test(arg)) {
43-
const [key, ...v] = arg.replace(/^--/, '').split('=')
44-
const val = v.join('=')
45-
options[key] = val === 'false' ? false : val === 'true' ? true : val
46-
} else if (/^--.+/.test(arg)) {
47-
options[arg.replace(/^--/, '')] = true
48-
} else if (options.path === undefined) {
49-
options.path = arg
50-
} else {
51-
options._.push(arg)
13+
const parse = (...noptArgs) => {
14+
const binOnlyOpts = {
15+
command: String,
16+
loglevel: String,
17+
colors: Boolean,
18+
timing: ['always', Boolean],
19+
logfile: String,
20+
}
21+
22+
const arbOpts = {
23+
add: Array,
24+
rm: Array,
25+
omit: Array,
26+
update: Array,
27+
workspaces: Array,
28+
global: Boolean,
29+
force: Boolean,
30+
'global-style': Boolean,
31+
'prefer-dedupe': Boolean,
32+
'legacy-peer-deps': Boolean,
33+
'update-all': Boolean,
34+
before: Date,
35+
path: path,
36+
cache: path,
37+
...binOnlyOpts,
38+
}
39+
40+
const short = {
41+
quiet: ['--loglevel', 'warn'],
42+
logs: ['--logfile', 'true'],
43+
w: '--workspaces',
44+
g: '--global',
45+
f: '--force',
46+
}
47+
48+
const defaults = {
49+
// key order is important for command and path
50+
// since they shift positional args
51+
// command is 1st, path is 2nd
52+
command: (o) => o.argv.remain.shift(),
53+
path: (o) => cleanPath(o.argv.remain.shift() || '.'),
54+
colors: has(process.env, 'NO_COLOR') ? false : !!process.stderr.isTTY,
55+
loglevel: 'silly',
56+
timing: (o) => o.loglevel === 'silly',
57+
cache: `${process.env.HOME}/.npm/_cacache`,
58+
}
59+
60+
const derived = [
61+
// making update either `all` or an array of names but not both
62+
({ updateAll: all, update: names, ...o }) => {
63+
if (all || names) {
64+
o.update = all != null ? { all } : { names }
65+
}
66+
return o
67+
},
68+
({ logfile, ...o }) => {
69+
// logfile is parsed as a string so if its true or set but empty
70+
// then set the default logfile
71+
if (logfile === 'true' || logfile === '') {
72+
logfile = `arb-log-${new Date().toISOString().replace(/[.:]/g, '_')}.log`
73+
}
74+
// then parse it the same as nopt parses other paths
75+
if (logfile) {
76+
o.logfile = cleanPath(logfile)
77+
}
78+
return o
79+
},
80+
]
81+
82+
const transforms = [
83+
// Camelcase all top level keys
84+
(o) => {
85+
const entries = Object.entries(o).map(([k, v]) => [
86+
k.replace(/-./g, s => s[1].toUpperCase()),
87+
v,
88+
])
89+
return Object.fromEntries(entries)
90+
},
91+
// Set defaults on unset keys
92+
(o) => {
93+
for (const [k, v] of Object.entries(defaults)) {
94+
if (!has(o, k)) {
95+
o[k] = typeof v === 'function' ? v(o) : v
96+
}
97+
}
98+
return o
99+
},
100+
// Set/unset derived values
101+
...derived.map((derive) => (o) => derive(o) || o),
102+
// Separate bin and arborist options
103+
({ argv: { remain: _ }, ...o }) => {
104+
const bin = { _ }
105+
for (const k of Object.keys(binOnlyOpts)) {
106+
if (has(o, k)) {
107+
bin[k] = o[k]
108+
delete o[k]
109+
}
110+
}
111+
return { bin, arb: o }
112+
},
113+
]
114+
115+
let options = nopt(arbOpts, short, ...noptArgs)
116+
for (const t of transforms) {
117+
options = t(options)
52118
}
53-
}
54119

55-
if (options.path === undefined) {
56-
options.path = '.'
120+
return options
57121
}
58122

59-
console.error(options)
123+
module.exports = parse()
+2-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
const { inspect } = require('util')
2-
const { quiet } = require('./options.js')
2+
const log = require('./logging.js')
33

4-
module.exports = quiet ? () => {}
5-
: tree => console.log(inspect(tree.toJSON(), { depth: Infinity }))
4+
module.exports = tree => log.info(inspect(tree.toJSON(), { depth: Infinity }))

‎workspaces/arborist/bin/lib/timers.js

+17-15
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
1-
const timers = Object.create(null)
2-
const { format } = require('util')
3-
const options = require('./options.js')
1+
const { bin: options } = require('./options.js')
2+
const log = require('./logging.js')
3+
4+
const timers = new Map()
5+
const finished = new Map()
46

57
process.on('time', name => {
6-
if (timers[name]) {
8+
if (timers.has(name)) {
79
throw new Error('conflicting timer! ' + name)
810
}
9-
timers[name] = process.hrtime()
11+
timers.set(name, process.hrtime.bigint())
1012
})
1113

12-
const dim = process.stderr.isTTY ? msg => `\x1B[2m${msg}\x1B[22m` : m => m
13-
const red = process.stderr.isTTY ? msg => `\x1B[31m${msg}\x1B[39m` : m => m
1414
process.on('timeEnd', name => {
15-
if (!timers[name]) {
15+
if (!timers.has(name)) {
1616
throw new Error('timer not started! ' + name)
1717
}
18-
const res = process.hrtime(timers[name])
19-
delete timers[name]
20-
const msg = format(`${process.pid} ${name}`, res[0] * 1e3 + res[1] / 1e6)
21-
if (options.timers !== false) {
22-
console.error(dim(msg))
18+
const elapsed = Number(process.hrtime.bigint() - timers.get(name))
19+
timers.delete(name)
20+
finished.set(name, elapsed)
21+
if (options.timing) {
22+
log.info('timeEnd', `${name} ${elapsed / 1e9}s`, log.meta({ force: options.timing === 'always' }))
2323
}
2424
})
2525

2626
process.on('exit', () => {
27-
for (const name of Object.keys(timers)) {
28-
console.error(red('Dangling timer:'), name)
27+
for (const name of timers.keys()) {
28+
log.error('timeError', 'Dangling timer:', name)
2929
process.exitCode = 1
3030
}
3131
})
32+
33+
module.exports = finished

‎workspaces/arborist/bin/license.js

+44-35
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,48 @@
1-
const Arborist = require('../')
21
const localeCompare = require('@isaacs/string-locale-compare')('en')
3-
const options = require('./lib/options.js')
4-
require('./lib/logging.js')
5-
require('./lib/timers.js')
2+
const Arborist = require('../')
3+
const log = require('./lib/logging.js')
64

7-
const a = new Arborist(options)
8-
const query = options._.shift()
5+
module.exports = (options, time) => {
6+
const query = options._.shift()
7+
const a = new Arborist(options)
8+
return a
9+
.loadVirtual()
10+
.then(tree => {
11+
// only load the actual tree if the virtual one doesn't have modern metadata
12+
if (!tree.meta || !(tree.meta.originalLockfileVersion >= 2)) {
13+
throw 'load actual'
14+
} else {
15+
return tree
16+
}
17+
}).catch((er) => {
18+
log.error('loading actual tree', er)
19+
return a.loadActual()
20+
})
21+
.then(time)
22+
.then(({ result: tree }) => {
23+
const output = []
24+
if (!query) {
25+
const set = []
26+
for (const license of tree.inventory.query('license')) {
27+
set.push([tree.inventory.query('license', license).size, license])
28+
}
929

10-
a.loadVirtual().then(tree => {
11-
// only load the actual tree if the virtual one doesn't have modern metadata
12-
if (!tree.meta || !(tree.meta.originalLockfileVersion >= 2)) {
13-
throw 'load actual'
14-
} else {
15-
return tree
16-
}
17-
}).catch((er) => {
18-
console.error('loading actual tree', er)
19-
return a.loadActual()
20-
}).then(tree => {
21-
if (!query) {
22-
const set = []
23-
for (const license of tree.inventory.query('license')) {
24-
set.push([tree.inventory.query('license', license).size, license])
25-
}
30+
for (const [count, license] of set.sort((a, b) =>
31+
a[1] && b[1] ? b[0] - a[0] || localeCompare(a[1], b[1])
32+
: a[1] ? -1
33+
: b[1] ? 1
34+
: 0)) {
35+
output.push(`${count} ${license}`)
36+
log.info(count, license)
37+
}
38+
} else {
39+
for (const node of tree.inventory.query('license', query === 'undefined' ? undefined : query)) {
40+
const msg = `${node.name} ${node.location} ${node.package.description || ''}`
41+
output.push(msg)
42+
log.info(msg)
43+
}
44+
}
2645

27-
for (const [count, license] of set.sort((a, b) =>
28-
a[1] && b[1] ? b[0] - a[0] || localeCompare(a[1], b[1])
29-
: a[1] ? -1
30-
: b[1] ? 1
31-
: 0)) {
32-
console.log(count, license)
33-
}
34-
} else {
35-
for (const node of tree.inventory.query('license', query === 'undefined' ? undefined : query)) {
36-
console.log(`${node.name} ${node.location} ${node.package.description || ''}`)
37-
}
38-
}
39-
})
46+
return output.join('\n')
47+
})
48+
}

‎workspaces/arborist/bin/prune.js

+21-22
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
const Arborist = require('../')
22

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')
3+
const printTree = require('./lib/print-tree.js')
4+
const log = require('./lib/logging.js')
75

86
const printDiff = diff => {
97
const { depth } = require('treeverse')
@@ -15,13 +13,13 @@ const printDiff = diff => {
1513
}
1614
switch (d.action) {
1715
case 'REMOVE':
18-
console.error('REMOVE', d.actual.location)
16+
log.info('REMOVE', d.actual.location)
1917
break
2018
case 'ADD':
21-
console.error('ADD', d.ideal.location, d.ideal.resolved)
19+
log.info('ADD', d.ideal.location, d.ideal.resolved)
2220
break
2321
case 'CHANGE':
24-
console.error('CHANGE', d.actual.location, {
22+
log.info('CHANGE', d.actual.location, {
2523
from: d.actual.resolved,
2624
to: d.ideal.resolved,
2725
})
@@ -32,18 +30,19 @@ const printDiff = diff => {
3230
})
3331
}
3432

35-
const start = process.hrtime()
36-
process.emit('time', 'install')
37-
const arb = new Arborist(options)
38-
arb.prune(options).then(tree => {
39-
process.emit('timeEnd', 'install')
40-
const end = process.hrtime(start)
41-
print(tree)
42-
if (options.dryRun) {
43-
printDiff(arb.diff)
44-
}
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-
}
49-
}).catch(er => console.error(require('util').inspect(er, { depth: Infinity })))
33+
module.exports = (options, time) => {
34+
const arb = new Arborist(options)
35+
return arb
36+
.prune(options)
37+
.then(time)
38+
.then(async ({ timing, result: tree }) => {
39+
printTree(tree)
40+
if (options.dryRun) {
41+
printDiff(arb.diff)
42+
}
43+
if (tree.meta && options.save) {
44+
await tree.meta.save()
45+
}
46+
return `resolved ${tree.inventory.size} deps in ${timing.seconds}`
47+
})
48+
}

‎workspaces/arborist/bin/reify.js

+21-22
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
const Arborist = require('../')
22

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')
3+
const printTree = require('./lib/print-tree.js')
4+
const log = require('./lib/logging.js')
75

86
const printDiff = diff => {
97
const { depth } = require('treeverse')
@@ -15,13 +13,13 @@ const printDiff = diff => {
1513
}
1614
switch (d.action) {
1715
case 'REMOVE':
18-
console.error('REMOVE', d.actual.location)
16+
log.info('REMOVE', d.actual.location)
1917
break
2018
case 'ADD':
21-
console.error('ADD', d.ideal.location, d.ideal.resolved)
19+
log.info('ADD', d.ideal.location, d.ideal.resolved)
2220
break
2321
case 'CHANGE':
24-
console.error('CHANGE', d.actual.location, {
22+
log.info('CHANGE', d.actual.location, {
2523
from: d.actual.resolved,
2624
to: d.ideal.resolved,
2725
})
@@ -32,18 +30,19 @@ const printDiff = diff => {
3230
})
3331
}
3432

35-
const start = process.hrtime()
36-
process.emit('time', 'install')
37-
const arb = new Arborist(options)
38-
arb.reify(options).then(tree => {
39-
process.emit('timeEnd', 'install')
40-
const end = process.hrtime(start)
41-
print(tree)
42-
if (options.dryRun) {
43-
printDiff(arb.diff)
44-
}
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-
}
49-
}).catch(er => console.error(require('util').inspect(er, { depth: Infinity })))
33+
module.exports = (options, time) => {
34+
const arb = new Arborist(options)
35+
return arb
36+
.reify(options)
37+
.then(time)
38+
.then(async ({ timing, result: tree }) => {
39+
printTree(tree)
40+
if (options.dryRun) {
41+
printDiff(arb.diff)
42+
}
43+
if (tree.meta && options.save) {
44+
await tree.meta.save()
45+
}
46+
return `resolved ${tree.inventory.size} deps in ${timing.seconds}`
47+
})
48+
}

‎workspaces/arborist/bin/shrinkwrap.js

+5-10
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
const Shrinkwrap = require('../lib/shrinkwrap.js')
2-
const options = require('./lib/options.js')
3-
require('./lib/logging.js')
4-
require('./lib/timers.js')
52

6-
const { quiet } = options
7-
Shrinkwrap.load(options)
8-
.then(s => quiet || console.log(JSON.stringify(s.commit(), 0, 2)))
9-
.catch(er => {
10-
console.error('shrinkwrap load failure', er)
11-
process.exit(1)
12-
})
3+
module.exports = (options, time) => Shrinkwrap
4+
.load(options)
5+
.then((s) => s.commit())
6+
.then(time)
7+
.then(({ result: s }) => JSON.stringify(s, 0, 2))

‎workspaces/arborist/bin/virtual.js

+11-15
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
const Arborist = require('../')
22

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')
3+
const printTree = require('./lib/print-tree.js')
74

8-
const start = process.hrtime()
9-
new Arborist(options).loadVirtual().then(tree => {
10-
const end = process.hrtime(start)
11-
if (!options.quiet) {
12-
print(tree)
13-
}
14-
if (options.save) {
15-
tree.meta.save()
16-
}
17-
console.error(`read ${tree.inventory.size} deps in ${end[0] * 1000 + end[1] / 1e6}ms`)
18-
}).catch(er => console.error(er))
5+
module.exports = (options, time) => new Arborist(options)
6+
.loadVirtual()
7+
.then(time)
8+
.then(async ({ timing, result: tree }) => {
9+
printTree(tree)
10+
if (options.save) {
11+
await tree.meta.save()
12+
}
13+
return `read ${tree.inventory.size} deps in ${timing.ms}`
14+
})

‎workspaces/arborist/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"json-stringify-nice": "^1.1.4",
2020
"mkdirp": "^1.0.4",
2121
"mkdirp-infer-owner": "^2.0.0",
22+
"nopt": "^5.0.0",
2223
"npm-install-checks": "^4.0.0",
2324
"npm-package-arg": "^9.0.0",
2425
"npm-pick-manifest": "^7.0.0",

0 commit comments

Comments
 (0)
Please sign in to comment.