fund.js 6.4 KB

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