codekingpro/portable-devtools
114k
1// pass in an arborist object, and it'll output the data about what
2// was done, what was audited, etc.
3//
4// added ## packages, removed ## packages, and audited ## packages in 19.157s
5//
6// 1 package is looking for funding
7// run `npm fund` for details
8//
9// found 37 vulnerabilities (5 low, 7 moderate, 25 high)
10// run `npm audit fix` to fix them, or `npm audit` for details
11
12const { log, output } = require('proc-log')
13const { depth } = require('treeverse')
14const ms = require('ms')
15const npmAuditReport = require('npm-audit-report')
16const { readTree: getFundingInfo } = require('libnpmfund')
17const auditError = require('./audit-error.js')
18
19// TODO: output JSON if flatOptions.json is true
20const reifyOutput = (npm, arb) => {
21 const { diff, actualTree } = arb
22
23 // note: fails and crashes if we're running audit fix and there was an error which is a good thing, because there's no point printing all this other stuff in that case!
24 const auditReport = auditError(npm, arb.auditReport) ? null : arb.auditReport
25
26 // don't print any info in --silent mode, but we still need to set the exitCode properly from the audit report, if we have one.
27 if (npm.silent) {
28 getAuditReport(npm, auditReport)
29 return
30 }
31
32 const summary = {
33 add: [],
34 added: 0,
35 // audit gets added later
36 audited: auditReport && !auditReport.error ? actualTree.inventory.size : 0,
37 change: [],
38 changed: 0,
39 funding: 0,
40 remove: [],
41 removed: 0,
42 }
43
44 if (diff) {
45 const showDiff = npm.config.get('dry-run') || npm.config.get('long')
46 const chalk = npm.chalk
47
48 depth({
49 tree: diff,
50 visit: d => {
51 switch (d.action) {
52 case 'REMOVE':
53 if (showDiff) {
54 output.standard(`${chalk.blue('remove')} ${d.actual.name} ${d.actual.package.version}`)
55 }
56 summary.removed++
57 summary.remove.push({
58 name: d.actual.name,
59 version: d.actual.package.version,
60 path: d.actual.path,
61 })
62 break
63 case 'ADD':
64 if (showDiff) {
65 output.standard(`${chalk.green('add')} ${d.ideal.name} ${d.ideal.package.version}`)
66 }
67 if (actualTree.inventory.has(d.ideal)) {
68 summary.added++
69 summary.add.push({
70 name: d.ideal.name,
71 version: d.ideal.package.version,
72 path: d.ideal.path,
73 })
74 }
75 break
76 case 'CHANGE':
77 if (showDiff) {
78 output.standard(`${chalk.cyan('change')} ${d.actual.name} ${d.actual.package.version} => ${d.ideal.package.version}`)
79 }
80 summary.changed++
81 summary.change.push({
82 from: {
83 name: d.actual.name,
84 version: d.actual.package.version,
85 path: d.actual.path,
86 },
87 to: {
88 name: d.ideal.name,
89 version: d.ideal.package.version,
90 path: d.ideal.path,
91 },
92 })
93 break
94 default:
95 return
96 }
97 const node = d.actual || d.ideal
98 log.silly(d.action, node.location)
99 },
100 getChildren: d => d.children,
101 })
102 }
103
104 if (npm.flatOptions.fund) {
105 const fundingInfo = getFundingInfo(actualTree, { countOnly: true })
106 summary.funding = fundingInfo.length
107 }
108
109 if (npm.flatOptions.json) {
110 if (auditReport) {
111 // call this to set the exit code properly
112 getAuditReport(npm, auditReport)
113 summary.audit = npm.command === 'audit' ? auditReport
114 : auditReport.toJSON().metadata
115 }
116 output.buffer(summary)
117 } else {
118 packagesChangedMessage(npm, summary)
119 packagesFundingMessage(npm, summary)
120 printAuditReport(npm, auditReport)
121 }
122}
123
124// if we're running `npm audit fix`, then we print the full audit report at the end if there's still stuff, because it's silly for `npm audit` to tell you to run `npm audit` for details.
125// otherwise, use the summary report.
126// if we get here, we know it's not quiet or json.
127// If the loglevel is silent, then we just run the report to get the exitCode set appropriately.
128const printAuditReport = (npm, report) => {
129 const res = getAuditReport(npm, report)
130 if (!res || !res.report) {
131 return
132 }
133 output.standard(`\n${res.report}`)
134}
135
136const getAuditReport = (npm, report) => {
137 if (!report) {
138 return
139 }
140
141 // when in silent mode, we print nothing.
142 // the JSON output is going to just JSON.stringify() the report object.
143 const reporter = npm.silent ? 'quiet'
144 : npm.flatOptions.json ? 'quiet'
145 : npm.command !== 'audit' ? 'install'
146 : 'detail'
147 const defaultAuditLevel = npm.command !== 'audit' ? 'none' : 'low'
148 const auditLevel = npm.flatOptions.auditLevel || defaultAuditLevel
149
150 const res = npmAuditReport(report, {
151 reporter,
152 ...npm.flatOptions,
153 auditLevel,
154 chalk: npm.chalk,
155 })
156 if (npm.command === 'audit') {
157 process.exitCode = process.exitCode || res.exitCode
158 }
159 return res
160}
161
162const packagesChangedMessage = (npm, { added, removed, changed, audited }) => {
163 const msg = ['\n']
164 if (added === 0 && removed === 0 && changed === 0) {
165 msg.push('up to date')
166 if (audited) {
167 msg.push(', ')
168 }
169 } else {
170 if (added) {
171 msg.push(`added ${added} package${added === 1 ? '' : 's'}`)
172 }
173
174 if (removed) {
175 if (added) {
176 msg.push(', ')
177 }
178
179 if (added && !audited && !changed) {
180 msg.push('and ')
181 }
182
183 msg.push(`removed ${removed} package${removed === 1 ? '' : 's'}`)
184 }
185 if (changed) {
186 if (added || removed) {
187 msg.push(', ')
188 }
189
190 if (!audited && (added || removed)) {
191 msg.push('and ')
192 }
193
194 msg.push(`changed ${changed} package${changed === 1 ? '' : 's'}`)
195 }
196 if (audited) {
197 msg.push(', and ')
198 }
199 }
200 if (audited) {
201 msg.push(`audited ${audited} package${audited === 1 ? '' : 's'}`)
202 }
203
204 msg.push(` in ${ms(Date.now() - npm.started)}`)
205 output.standard(msg.join(''))
206}
207
208const packagesFundingMessage = (npm, { funding }) => {
209 if (!funding) {
210 return
211 }
212
213 output.standard()
214 const pkg = funding === 1 ? 'package' : 'packages'
215 const is = funding === 1 ? 'is' : 'are'
216 output.standard(`${funding} ${pkg} ${is} looking for funding`)
217 output.standard(' run `npm fund` for details')
218}
219
220module.exports = reifyOutput
221 