reify-output.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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. const { log, output } = require('proc-log')
  12. const { depth } = require('treeverse')
  13. const ms = require('ms')
  14. const npmAuditReport = require('npm-audit-report')
  15. const { readTree: getFundingInfo } = require('libnpmfund')
  16. const auditError = require('./audit-error.js')
  17. // TODO: output JSON if flatOptions.json is true
  18. const reifyOutput = (npm, arb) => {
  19. const { diff, actualTree } = arb
  20. // 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!
  21. const auditReport = auditError(npm, arb.auditReport) ? null : arb.auditReport
  22. // 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.
  23. if (npm.silent) {
  24. getAuditReport(npm, auditReport)
  25. return
  26. }
  27. const summary = {
  28. add: [],
  29. added: 0,
  30. // audit gets added later
  31. audited: auditReport && !auditReport.error ? actualTree.inventory.size : 0,
  32. change: [],
  33. changed: 0,
  34. funding: 0,
  35. remove: [],
  36. removed: 0,
  37. }
  38. if (diff) {
  39. const showDiff = npm.config.get('dry-run') || npm.config.get('long')
  40. const chalk = npm.chalk
  41. depth({
  42. tree: diff,
  43. visit: d => {
  44. switch (d.action) {
  45. case 'REMOVE':
  46. if (showDiff) {
  47. output.standard(`${chalk.blue('remove')} ${d.actual.name} ${d.actual.package.version}`)
  48. }
  49. summary.removed++
  50. summary.remove.push({
  51. name: d.actual.name,
  52. version: d.actual.package.version,
  53. path: d.actual.path,
  54. })
  55. break
  56. case 'ADD':
  57. if (showDiff) {
  58. output.standard(`${chalk.green('add')} ${d.ideal.name} ${d.ideal.package.version}`)
  59. }
  60. if (actualTree.inventory.has(d.ideal)) {
  61. summary.added++
  62. summary.add.push({
  63. name: d.ideal.name,
  64. version: d.ideal.package.version,
  65. path: d.ideal.path,
  66. })
  67. }
  68. break
  69. case 'CHANGE':
  70. if (showDiff) {
  71. output.standard(`${chalk.cyan('change')} ${d.actual.name} ${d.actual.package.version} => ${d.ideal.package.version}`)
  72. }
  73. summary.changed++
  74. summary.change.push({
  75. from: {
  76. name: d.actual.name,
  77. version: d.actual.package.version,
  78. path: d.actual.path,
  79. },
  80. to: {
  81. name: d.ideal.name,
  82. version: d.ideal.package.version,
  83. path: d.ideal.path,
  84. },
  85. })
  86. break
  87. default:
  88. return
  89. }
  90. const node = d.actual || d.ideal
  91. log.silly(d.action, node.location)
  92. },
  93. getChildren: d => d.children,
  94. })
  95. }
  96. if (npm.flatOptions.fund) {
  97. const fundingInfo = getFundingInfo(actualTree, { countOnly: true })
  98. summary.funding = fundingInfo.length
  99. }
  100. if (npm.flatOptions.json) {
  101. if (auditReport) {
  102. // call this to set the exit code properly
  103. getAuditReport(npm, auditReport)
  104. summary.audit = npm.command === 'audit' ? auditReport
  105. : auditReport.toJSON().metadata
  106. }
  107. output.buffer(summary)
  108. } else {
  109. packagesChangedMessage(npm, summary)
  110. packagesFundingMessage(npm, summary)
  111. printAuditReport(npm, auditReport)
  112. }
  113. }
  114. // 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.
  115. // otherwise, use the summary report.
  116. // if we get here, we know it's not quiet or json.
  117. // If the loglevel is silent, then we just run the report to get the exitCode set appropriately.
  118. const printAuditReport = (npm, report) => {
  119. const res = getAuditReport(npm, report)
  120. if (!res || !res.report) {
  121. return
  122. }
  123. output.standard(`\n${res.report}`)
  124. }
  125. const getAuditReport = (npm, report) => {
  126. if (!report) {
  127. return
  128. }
  129. // when in silent mode, we print nothing.
  130. // the JSON output is going to just JSON.stringify() the report object.
  131. const reporter = npm.silent ? 'quiet'
  132. : npm.flatOptions.json ? 'quiet'
  133. : npm.command !== 'audit' ? 'install'
  134. : 'detail'
  135. const defaultAuditLevel = npm.command !== 'audit' ? 'none' : 'low'
  136. const auditLevel = npm.flatOptions.auditLevel || defaultAuditLevel
  137. const res = npmAuditReport(report, {
  138. reporter,
  139. ...npm.flatOptions,
  140. auditLevel,
  141. chalk: npm.chalk,
  142. })
  143. if (npm.command === 'audit') {
  144. process.exitCode = process.exitCode || res.exitCode
  145. }
  146. return res
  147. }
  148. const packagesChangedMessage = (npm, { added, removed, changed, audited }) => {
  149. const msg = ['\n']
  150. if (added === 0 && removed === 0 && changed === 0) {
  151. msg.push('up to date')
  152. if (audited) {
  153. msg.push(', ')
  154. }
  155. } else {
  156. if (added) {
  157. msg.push(`added ${added} package${added === 1 ? '' : 's'}`)
  158. }
  159. if (removed) {
  160. if (added) {
  161. msg.push(', ')
  162. }
  163. if (added && !audited && !changed) {
  164. msg.push('and ')
  165. }
  166. msg.push(`removed ${removed} package${removed === 1 ? '' : 's'}`)
  167. }
  168. if (changed) {
  169. if (added || removed) {
  170. msg.push(', ')
  171. }
  172. if (!audited && (added || removed)) {
  173. msg.push('and ')
  174. }
  175. msg.push(`changed ${changed} package${changed === 1 ? '' : 's'}`)
  176. }
  177. if (audited) {
  178. msg.push(', and ')
  179. }
  180. }
  181. if (audited) {
  182. msg.push(`audited ${audited} package${audited === 1 ? '' : 's'}`)
  183. }
  184. msg.push(` in ${ms(Date.now() - npm.started)}`)
  185. output.standard(msg.join(''))
  186. }
  187. const packagesFundingMessage = (npm, { funding }) => {
  188. if (!funding) {
  189. return
  190. }
  191. output.standard()
  192. const pkg = funding === 1 ? 'package' : 'packages'
  193. const is = funding === 1 ? 'is' : 'are'
  194. output.standard(`${funding} ${pkg} ${is} looking for funding`)
  195. output.standard(' run `npm fund` for details')
  196. }
  197. module.exports = reifyOutput