fund.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. 'use strict'
  2. const path = require('path')
  3. const archy = require('archy')
  4. const figgyPudding = require('figgy-pudding')
  5. const readPackageTree = require('read-package-tree')
  6. const npm = require('./npm.js')
  7. const npmConfig = require('./config/figgy-config.js')
  8. const fetchPackageMetadata = require('./fetch-package-metadata.js')
  9. const computeMetadata = require('./install/deps.js').computeMetadata
  10. const readShrinkwrap = require('./install/read-shrinkwrap.js')
  11. const mutateIntoLogicalTree = require('./install/mutate-into-logical-tree.js')
  12. const output = require('./utils/output.js')
  13. const openUrl = require('./utils/open-url.js')
  14. const { getFundingInfo, retrieveFunding, validFundingField, flatCacheSymbol } = require('./utils/funding.js')
  15. const FundConfig = figgyPudding({
  16. browser: {}, // used by ./utils/open-url
  17. global: {},
  18. json: {},
  19. unicode: {},
  20. which: {}
  21. })
  22. module.exports = fundCmd
  23. const usage = require('./utils/usage')
  24. fundCmd.usage = usage(
  25. 'fund',
  26. 'npm fund [--json]',
  27. 'npm fund [--browser] [[<@scope>/]<pkg> [--which=<fundingSourceNumber>]'
  28. )
  29. fundCmd.completion = function (opts, cb) {
  30. const argv = opts.conf.argv.remain
  31. switch (argv[2]) {
  32. case 'fund':
  33. return cb(null, [])
  34. default:
  35. return cb(new Error(argv[2] + ' not recognized'))
  36. }
  37. }
  38. function printJSON (fundingInfo) {
  39. return JSON.stringify(fundingInfo, null, 2)
  40. }
  41. // the human-printable version does some special things that turned out to
  42. // be very verbose but hopefully not hard to follow: we stack up items
  43. // that have a shared url/type and make sure they're printed at the highest
  44. // level possible, in that process they also carry their dependencies along
  45. // with them, moving those up in the visual tree
  46. function printHuman (fundingInfo, opts) {
  47. const flatCache = fundingInfo[flatCacheSymbol]
  48. const { name, version } = fundingInfo
  49. const printableVersion = version ? `@${version}` : ''
  50. const items = Object.keys(flatCache).map((url) => {
  51. const deps = flatCache[url]
  52. const packages = deps.map((dep) => {
  53. const { name, version } = dep
  54. const printableVersion = version ? `@${version}` : ''
  55. return `${name}${printableVersion}`
  56. })
  57. return {
  58. label: url,
  59. nodes: [packages.join(', ')]
  60. }
  61. })
  62. return archy({ label: `${name}${printableVersion}`, nodes: items }, '', { unicode: opts.unicode })
  63. }
  64. function openFundingUrl (packageName, fundingSourceNumber, cb) {
  65. function getUrlAndOpen (packageMetadata) {
  66. const { funding } = packageMetadata
  67. const validSources = [].concat(retrieveFunding(funding)).filter(validFundingField)
  68. if (validSources.length === 1 || (fundingSourceNumber > 0 && fundingSourceNumber <= validSources.length)) {
  69. const { type, url } = validSources[fundingSourceNumber ? fundingSourceNumber - 1 : 0]
  70. const typePrefix = type ? `${type} funding` : 'Funding'
  71. const msg = `${typePrefix} available at the following URL`
  72. openUrl(url, msg, cb)
  73. } else if (!(fundingSourceNumber >= 1)) {
  74. validSources.forEach(({ type, url }, i) => {
  75. const typePrefix = type ? `${type} funding` : 'Funding'
  76. const msg = `${typePrefix} available at the following URL`
  77. console.log(`${i + 1}: ${msg}: ${url}`)
  78. })
  79. console.log('Run `npm fund [<@scope>/]<pkg> --which=1`, for example, to open the first funding URL listed in that package')
  80. cb()
  81. } else {
  82. const noFundingError = new Error(`No valid funding method available for: ${packageName}`)
  83. noFundingError.code = 'ENOFUND'
  84. throw noFundingError
  85. }
  86. }
  87. fetchPackageMetadata(
  88. packageName,
  89. '.',
  90. { fullMetadata: true },
  91. function (err, packageMetadata) {
  92. if (err) return cb(err)
  93. getUrlAndOpen(packageMetadata)
  94. }
  95. )
  96. }
  97. function fundCmd (args, cb) {
  98. const opts = FundConfig(npmConfig())
  99. const dir = path.resolve(npm.dir, '..')
  100. const packageName = args[0]
  101. const numberArg = opts.which
  102. const fundingSourceNumber = numberArg && parseInt(numberArg, 10)
  103. if (numberArg !== undefined && (String(fundingSourceNumber) !== numberArg || fundingSourceNumber < 1)) {
  104. const err = new Error('`npm fund [<@scope>/]<pkg> [--which=fundingSourceNumber]` must be given a positive integer')
  105. err.code = 'EFUNDNUMBER'
  106. throw err
  107. }
  108. if (opts.global) {
  109. const err = new Error('`npm fund` does not support global packages')
  110. err.code = 'EFUNDGLOBAL'
  111. throw err
  112. }
  113. if (packageName) {
  114. openFundingUrl(packageName, fundingSourceNumber, cb)
  115. return
  116. }
  117. readPackageTree(dir, function (err, tree) {
  118. if (err) {
  119. process.exitCode = 1
  120. return cb(err)
  121. }
  122. readShrinkwrap.andInflate(tree, function () {
  123. const fundingInfo = getFundingInfo(
  124. mutateIntoLogicalTree.asReadInstalled(
  125. computeMetadata(tree)
  126. )
  127. )
  128. const print = opts.json
  129. ? printJSON
  130. : printHuman
  131. output(
  132. print(
  133. fundingInfo,
  134. opts
  135. )
  136. )
  137. cb(err, tree)
  138. })
  139. })
  140. }