fund.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. const archy = require('archy')
  2. const Arborist = require('@npmcli/arborist')
  3. const chalk = require('chalk')
  4. const pacote = require('pacote')
  5. const semver = require('semver')
  6. const npa = require('npm-package-arg')
  7. const { depth } = require('treeverse')
  8. const {
  9. readTree: getFundingInfo,
  10. normalizeFunding,
  11. isValidFunding,
  12. } = require('libnpmfund')
  13. const completion = require('./utils/completion/installed-deep.js')
  14. const openUrl = require('./utils/open-url.js')
  15. const ArboristWorkspaceCmd = require('./workspaces/arborist-cmd.js')
  16. const getPrintableName = ({ name, version }) => {
  17. const printableVersion = version ? `@${version}` : ''
  18. return `${name}${printableVersion}`
  19. }
  20. class Fund extends ArboristWorkspaceCmd {
  21. /* istanbul ignore next - see test/lib/load-all-commands.js */
  22. static get description () {
  23. return 'Retrieve funding information'
  24. }
  25. /* istanbul ignore next - see test/lib/load-all-commands.js */
  26. static get name () {
  27. return 'fund'
  28. }
  29. /* istanbul ignore next - see test/lib/load-all-commands.js */
  30. static get params () {
  31. return [
  32. 'json',
  33. 'browser',
  34. 'unicode',
  35. 'workspace',
  36. 'which',
  37. ]
  38. }
  39. /* istanbul ignore next - see test/lib/load-all-commands.js */
  40. static get usage () {
  41. return ['[[<@scope>/]<pkg>]']
  42. }
  43. /* istanbul ignore next - see test/lib/load-all-commands.js */
  44. async completion (opts) {
  45. return completion(this.npm, opts)
  46. }
  47. exec (args, cb) {
  48. this.fund(args).then(() => cb()).catch(cb)
  49. }
  50. async fund (args) {
  51. const spec = args[0]
  52. const numberArg = this.npm.config.get('which')
  53. const fundingSourceNumber = numberArg && parseInt(numberArg, 10)
  54. const badFundingSourceNumber =
  55. numberArg !== null &&
  56. (String(fundingSourceNumber) !== numberArg || fundingSourceNumber < 1)
  57. if (badFundingSourceNumber) {
  58. const err = new Error('`npm fund [<@scope>/]<pkg> [--which=fundingSourceNumber]` must be given a positive integer')
  59. err.code = 'EFUNDNUMBER'
  60. throw err
  61. }
  62. if (this.npm.config.get('global')) {
  63. const err = new Error('`npm fund` does not support global packages')
  64. err.code = 'EFUNDGLOBAL'
  65. throw err
  66. }
  67. const where = this.npm.prefix
  68. const arb = new Arborist({ ...this.npm.flatOptions, path: where })
  69. const tree = await arb.loadActual()
  70. if (spec) {
  71. await this.openFundingUrl({
  72. path: where,
  73. tree,
  74. spec,
  75. fundingSourceNumber,
  76. })
  77. return
  78. }
  79. // TODO: add !workspacesEnabled option handling to libnpmfund
  80. const fundingInfo = getFundingInfo(tree, {
  81. ...this.flatOptions,
  82. log: this.npm.log,
  83. workspaces: this.workspaceNames,
  84. })
  85. if (this.npm.config.get('json'))
  86. this.npm.output(this.printJSON(fundingInfo))
  87. else
  88. this.npm.output(this.printHuman(fundingInfo))
  89. }
  90. printJSON (fundingInfo) {
  91. return JSON.stringify(fundingInfo, null, 2)
  92. }
  93. printHuman (fundingInfo) {
  94. const color = this.npm.color
  95. const unicode = this.npm.config.get('unicode')
  96. const seenUrls = new Map()
  97. const tree = obj =>
  98. archy(obj, '', { unicode })
  99. const result = depth({
  100. tree: fundingInfo,
  101. // composes human readable package name
  102. // and creates a new archy item for readable output
  103. visit: ({ name, version, funding }) => {
  104. const [fundingSource] = []
  105. .concat(normalizeFunding(funding))
  106. .filter(isValidFunding)
  107. const { url } = fundingSource || {}
  108. const pkgRef = getPrintableName({ name, version })
  109. let item = {
  110. label: pkgRef,
  111. }
  112. if (url) {
  113. item.label = tree({
  114. label: color ? chalk.bgBlack.white(url) : url,
  115. nodes: [pkgRef],
  116. }).trim()
  117. // stacks all packages together under the same item
  118. if (seenUrls.has(url)) {
  119. item = seenUrls.get(url)
  120. item.label += `, ${pkgRef}`
  121. return null
  122. } else
  123. seenUrls.set(url, item)
  124. }
  125. return item
  126. },
  127. // puts child nodes back into returned archy
  128. // output while also filtering out missing items
  129. leave: (item, children) => {
  130. if (item)
  131. item.nodes = children.filter(Boolean)
  132. return item
  133. },
  134. // turns tree-like object return by libnpmfund
  135. // into children to be properly read by treeverse
  136. getChildren: (node) =>
  137. Object.keys(node.dependencies || {})
  138. .map(key => ({
  139. name: key,
  140. ...node.dependencies[key],
  141. })),
  142. })
  143. const res = tree(result)
  144. return color ? chalk.reset(res) : res
  145. }
  146. async openFundingUrl ({ path, tree, spec, fundingSourceNumber }) {
  147. const arg = npa(spec, path)
  148. const retrievePackageMetadata = () => {
  149. if (arg.type === 'directory') {
  150. if (tree.path === arg.fetchSpec) {
  151. // matches cwd, e.g: npm fund .
  152. return tree.package
  153. } else {
  154. // matches any file path within current arborist inventory
  155. for (const item of tree.inventory.values()) {
  156. if (item.path === arg.fetchSpec)
  157. return item.package
  158. }
  159. }
  160. } else {
  161. // tries to retrieve a package from arborist inventory
  162. // by matching resulted package name from the provided spec
  163. const [item] = [...tree.inventory.query('name', arg.name)]
  164. .filter(i => semver.valid(i.package.version))
  165. .sort((a, b) => semver.rcompare(a.package.version, b.package.version))
  166. if (item)
  167. return item.package
  168. }
  169. }
  170. const { funding } = retrievePackageMetadata() ||
  171. await pacote.manifest(arg, this.npm.flatOptions).catch(() => ({}))
  172. const validSources = []
  173. .concat(normalizeFunding(funding))
  174. .filter(isValidFunding)
  175. const matchesValidSource =
  176. validSources.length === 1 ||
  177. (fundingSourceNumber > 0 && fundingSourceNumber <= validSources.length)
  178. if (matchesValidSource) {
  179. const index = fundingSourceNumber ? fundingSourceNumber - 1 : 0
  180. const { type, url } = validSources[index]
  181. const typePrefix = type ? `${type} funding` : 'Funding'
  182. const msg = `${typePrefix} available at the following URL`
  183. return openUrl(this.npm, url, msg)
  184. } else if (validSources.length && !(fundingSourceNumber >= 1)) {
  185. validSources.forEach(({ type, url }, i) => {
  186. const typePrefix = type ? `${type} funding` : 'Funding'
  187. const msg = `${typePrefix} available at the following URL`
  188. this.npm.output(`${i + 1}: ${msg}: ${url}`)
  189. })
  190. this.npm.output('Run `npm fund [<@scope>/]<pkg> --which=1`, for example, to open the first funding URL listed in that package')
  191. } else {
  192. const noFundingError = new Error(`No valid funding method available for: ${spec}`)
  193. noFundingError.code = 'ENOFUND'
  194. throw noFundingError
  195. }
  196. }
  197. }
  198. module.exports = Fund