reify-output.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  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 = require('npmlog')
  12. const { depth } = require('treeverse')
  13. const ms = require('ms')
  14. const auditReport = 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
  21. // which is a good thing, because there's no point printing all this other
  22. // stuff in that case!
  23. const auditReport = auditError(npm, arb.auditReport) ? null : arb.auditReport
  24. // don't print any info in --silent mode, but we still need to
  25. // set the exitCode properly from the audit report, if we have one.
  26. if (log.levels[log.level] > log.levels.error) {
  27. getAuditReport(npm, auditReport)
  28. return
  29. }
  30. const summary = {
  31. added: 0,
  32. removed: 0,
  33. changed: 0,
  34. audited: auditReport && !auditReport.error ? actualTree.inventory.size : 0,
  35. funding: 0,
  36. }
  37. if (diff) {
  38. depth({
  39. tree: diff,
  40. visit: d => {
  41. switch (d.action) {
  42. case 'REMOVE':
  43. summary.removed++
  44. break
  45. case 'ADD':
  46. actualTree.inventory.has(d.ideal) && summary.added++
  47. break
  48. case 'CHANGE':
  49. summary.changed++
  50. break
  51. default:
  52. return
  53. }
  54. const node = d.actual || d.ideal
  55. log.silly(d.action, node.location)
  56. },
  57. getChildren: d => d.children,
  58. })
  59. }
  60. if (npm.flatOptions.fund) {
  61. const fundingInfo = getFundingInfo(actualTree, { countOnly: true })
  62. summary.funding = fundingInfo.length
  63. }
  64. if (npm.flatOptions.json) {
  65. if (auditReport) {
  66. // call this to set the exit code properly
  67. getAuditReport(npm, auditReport)
  68. summary.audit = npm.command === 'audit' ? auditReport
  69. : auditReport.toJSON().metadata
  70. }
  71. npm.output(JSON.stringify(summary, 0, 2))
  72. } else {
  73. packagesChangedMessage(npm, summary)
  74. packagesFundingMessage(npm, summary)
  75. printAuditReport(npm, auditReport)
  76. }
  77. }
  78. // if we're running `npm audit fix`, then we print the full audit report
  79. // at the end if there's still stuff, because it's silly for `npm audit`
  80. // to tell you to run `npm audit` for details. otherwise, use the summary
  81. // report. if we get here, we know it's not quiet or json.
  82. // If the loglevel is set higher than 'error', then we just run the report
  83. // to get the exitCode set appropriately.
  84. const printAuditReport = (npm, report) => {
  85. const res = getAuditReport(npm, report)
  86. if (!res || !res.report)
  87. return
  88. npm.output(`\n${res.report}`)
  89. }
  90. const getAuditReport = (npm, report) => {
  91. if (!report)
  92. return
  93. // when in silent mode, we print nothing. the JSON output is
  94. // going to just JSON.stringify() the report object.
  95. const reporter = log.levels[log.level] > log.levels.error ? 'quiet'
  96. : npm.flatOptions.json ? 'quiet'
  97. : npm.command !== 'audit' ? 'install'
  98. : 'detail'
  99. const defaultAuditLevel = npm.command !== 'audit' ? 'none' : 'low'
  100. const auditLevel = npm.flatOptions.auditLevel || defaultAuditLevel
  101. const res = auditReport(report, {
  102. reporter,
  103. ...npm.flatOptions,
  104. auditLevel,
  105. })
  106. if (npm.command === 'audit')
  107. process.exitCode = process.exitCode || res.exitCode
  108. return res
  109. }
  110. const packagesChangedMessage = (npm, { added, removed, changed, audited }) => {
  111. const msg = ['\n']
  112. if (added === 0 && removed === 0 && changed === 0) {
  113. msg.push('up to date')
  114. if (audited)
  115. msg.push(', ')
  116. } else {
  117. if (added)
  118. msg.push(`added ${added} package${added === 1 ? '' : 's'}`)
  119. if (removed) {
  120. if (added)
  121. msg.push(', ')
  122. if (added && !audited && !changed)
  123. msg.push('and ')
  124. msg.push(`removed ${removed} package${removed === 1 ? '' : 's'}`)
  125. }
  126. if (changed) {
  127. if (added || removed)
  128. msg.push(', ')
  129. if (!audited && (added || removed))
  130. msg.push('and ')
  131. msg.push(`changed ${changed} package${changed === 1 ? '' : 's'}`)
  132. }
  133. if (audited)
  134. msg.push(', and ')
  135. }
  136. if (audited)
  137. msg.push(`audited ${audited} package${audited === 1 ? '' : 's'}`)
  138. msg.push(` in ${ms(Date.now() - npm.started)}`)
  139. npm.output(msg.join(''))
  140. }
  141. const packagesFundingMessage = (npm, { funding }) => {
  142. if (!funding)
  143. return
  144. npm.output('')
  145. const pkg = funding === 1 ? 'package' : 'packages'
  146. const is = funding === 1 ? 'is' : 'are'
  147. npm.output(`${funding} ${pkg} ${is} looking for funding`)
  148. npm.output(' run `npm fund` for details')
  149. }
  150. module.exports = reifyOutput