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