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