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