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