funding.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. 'use strict'
  2. const URL = require('url').URL
  3. exports.getFundingInfo = getFundingInfo
  4. exports.retrieveFunding = retrieveFunding
  5. exports.validFundingField = validFundingField
  6. const flatCacheSymbol = Symbol('npm flat cache')
  7. exports.flatCacheSymbol = flatCacheSymbol
  8. // supports object funding and string shorthand, or an array of these
  9. // if original was an array, returns an array; else returns the lone item
  10. function retrieveFunding (funding) {
  11. const sources = [].concat(funding || []).map(item => (
  12. typeof item === 'string'
  13. ? { url: item }
  14. : item
  15. ))
  16. return Array.isArray(funding) ? sources : sources[0]
  17. }
  18. // Is the value of a `funding` property of a `package.json`
  19. // a valid type+url for `npm fund` to display?
  20. function validFundingField (funding) {
  21. if (!funding) return false
  22. if (Array.isArray(funding)) {
  23. return funding.every(f => !Array.isArray(f) && validFundingField(f))
  24. }
  25. try {
  26. var parsed = new URL(funding.url || funding)
  27. } catch (error) {
  28. return false
  29. }
  30. if (
  31. parsed.protocol !== 'https:' &&
  32. parsed.protocol !== 'http:'
  33. ) return false
  34. return Boolean(parsed.host)
  35. }
  36. const empty = () => Object.create(null)
  37. function getFundingInfo (idealTree, opts) {
  38. let packageWithFundingCount = 0
  39. const flat = empty()
  40. const seen = new Set()
  41. const { countOnly } = opts || {}
  42. const _trailingDependencies = Symbol('trailingDependencies')
  43. function tracked (name, version) {
  44. const key = String(name) + String(version)
  45. if (seen.has(key)) {
  46. return true
  47. }
  48. seen.add(key)
  49. }
  50. function retrieveDependencies (dependencies) {
  51. const trailing = dependencies[_trailingDependencies]
  52. if (trailing) {
  53. return Object.assign(
  54. empty(),
  55. dependencies,
  56. trailing
  57. )
  58. }
  59. return dependencies
  60. }
  61. function hasDependencies (dependencies) {
  62. return dependencies && (
  63. Object.keys(dependencies).length ||
  64. dependencies[_trailingDependencies]
  65. )
  66. }
  67. function addToFlatCache (funding, dep) {
  68. [].concat(funding || []).forEach((f) => {
  69. const key = f.url
  70. if (!Array.isArray(flat[key])) {
  71. flat[key] = []
  72. }
  73. flat[key].push(dep)
  74. })
  75. }
  76. function attachFundingInfo (target, funding, dep) {
  77. if (funding && validFundingField(funding)) {
  78. target.funding = retrieveFunding(funding)
  79. if (!countOnly) {
  80. addToFlatCache(target.funding, dep)
  81. }
  82. packageWithFundingCount++
  83. }
  84. }
  85. function getFundingDependencies (tree) {
  86. const deps = tree && tree.dependencies
  87. if (!deps) return empty()
  88. const directDepsWithFunding = Object.keys(deps).map((key) => {
  89. const dep = deps[key]
  90. const { name, funding, version } = dep
  91. // avoids duplicated items within the funding tree
  92. if (tracked(name, version)) return empty()
  93. const fundingItem = {}
  94. if (version) {
  95. fundingItem.version = version
  96. }
  97. attachFundingInfo(fundingItem, funding, dep)
  98. return {
  99. dep,
  100. fundingItem
  101. }
  102. })
  103. return directDepsWithFunding.reduce((res, { dep: directDep, fundingItem }, i) => {
  104. if (!fundingItem || fundingItem.length === 0) return res
  105. // recurse
  106. const transitiveDependencies = directDep.dependencies &&
  107. Object.keys(directDep.dependencies).length > 0 &&
  108. getFundingDependencies(directDep)
  109. // if we're only counting items there's no need
  110. // to add all the data to the resulting object
  111. if (countOnly) return null
  112. if (hasDependencies(transitiveDependencies)) {
  113. fundingItem.dependencies = retrieveDependencies(transitiveDependencies)
  114. }
  115. if (fundingItem.funding && fundingItem.funding.length !== 0) {
  116. res[directDep.name] = fundingItem
  117. } else if (fundingItem.dependencies) {
  118. res[_trailingDependencies] =
  119. Object.assign(
  120. empty(),
  121. res[_trailingDependencies],
  122. fundingItem.dependencies
  123. )
  124. }
  125. return res
  126. }, countOnly ? null : empty())
  127. }
  128. const idealTreeDependencies = getFundingDependencies(idealTree)
  129. const result = {
  130. length: packageWithFundingCount
  131. }
  132. if (!countOnly) {
  133. result.name = idealTree.name || idealTree.path
  134. if (idealTree && idealTree.version) {
  135. result.version = idealTree.version
  136. }
  137. if (idealTree && idealTree.funding) {
  138. result.funding = retrieveFunding(idealTree.funding)
  139. }
  140. result.dependencies = retrieveDependencies(idealTreeDependencies)
  141. result[flatCacheSymbol] = flat
  142. }
  143. return result
  144. }