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