codekingpro/portable-devtools
114k
1'use strict'
2
3const URL = require('node: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 }) => {
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 