1'use strict' 2 3const path = require('path') 4 5const archy = require('archy') 6const figgyPudding = require('figgy-pudding') 7const readPackageTree = require('read-package-tree') 8 9const npm = require('./npm.js') 10const npmConfig = require('./config/figgy-config.js') 11const fetchPackageMetadata = require('./fetch-package-metadata.js') 12const computeMetadata = require('./install/deps.js').computeMetadata 13const readShrinkwrap = require('./install/read-shrinkwrap.js') 14const mutateIntoLogicalTree = require('./install/mutate-into-logical-tree.js') 15const output = require('./utils/output.js') 16const openUrl = require('./utils/open-url.js') 17const { getFundingInfo, retrieveFunding, validFundingField, flatCacheSymbol } = require('./utils/funding.js') 18 19const FundConfig = figgyPudding({ 20 browser: {}, // used by ./utils/open-url 21 global: {}, 22 json: {}, 23 unicode: {}, 24 which: {} 25}) 26 27module.exports = fundCmd 28 29const usage = require('./utils/usage') 30fundCmd.usage = usage( 31 'fund', 32 'npm fund [--json]', 33 'npm fund [--browser] [[<@scope>/]<pkg> [--which=<fundingSourceNumber>]' 34) 35 36fundCmd.completion = function (opts, cb) { 37 const argv = opts.conf.argv.remain 38 switch (argv[2]) { 39 case 'fund': 40 return cb(null, []) 41 default: 42 return cb(new Error(argv[2] + ' not recognized')) 43 } 44} 45 46function printJSON (fundingInfo) { 47 return JSON.stringify(fundingInfo, null, 2) 48} 49 50// the human-printable version does some special things that turned out to 51// be very verbose but hopefully not hard to follow: we stack up items 52// that have a shared url/type and make sure they're printed at the highest 53// level possible, in that process they also carry their dependencies along 54// with them, moving those up in the visual tree 55function printHuman (fundingInfo, opts) { 56 const flatCache = fundingInfo[flatCacheSymbol] 57 58 const { name, version } = fundingInfo 59 const printableVersion = version ? `@${version}` : '' 60 61 const items = Object.keys(flatCache).map((url) => { 62 const deps = flatCache[url] 63 64 const packages = deps.map((dep) => { 65 const { name, version } = dep 66 67 const printableVersion = version ? `@${version}` : '' 68 return `${name}${printableVersion}` 69 }) 70 71 return { 72 label: url, 73 nodes: [packages.join(', ')] 74 } 75 }) 76 77 return archy({ label: `${name}${printableVersion}`, nodes: items }, '', { unicode: opts.unicode }) 78} 79 80function openFundingUrl (packageName, fundingSourceNumber, cb) { 81 function getUrlAndOpen (packageMetadata) { 82 const { funding } = packageMetadata 83 const validSources = [].concat(retrieveFunding(funding)).filter(validFundingField) 84 85 if (validSources.length === 1 || (fundingSourceNumber > 0 && fundingSourceNumber <= validSources.length)) { 86 const { type, url } = validSources[fundingSourceNumber ? fundingSourceNumber - 1 : 0] 87 const typePrefix = type ? `${type} funding` : 'Funding' 88 const msg = `${typePrefix} available at the following URL` 89 openUrl(url, msg, cb) 90 } else if (!(fundingSourceNumber >= 1)) { 91 validSources.forEach(({ type, url }, i) => { 92 const typePrefix = type ? `${type} funding` : 'Funding' 93 const msg = `${typePrefix} available at the following URL` 94 console.log(`${i + 1}: ${msg}: ${url}`) 95 }) 96 console.log('Run `npm fund [<@scope>/]<pkg> --which=1`, for example, to open the first funding URL listed in that package') 97 cb() 98 } else { 99 const noFundingError = new Error(`No valid funding method available for: ${packageName}`) 100 noFundingError.code = 'ENOFUND' 101 102 throw noFundingError 103 } 104 } 105 106 fetchPackageMetadata( 107 packageName, 108 '.', 109 { fullMetadata: true }, 110 function (err, packageMetadata) { 111 if (err) return cb(err) 112 getUrlAndOpen(packageMetadata) 113 } 114 ) 115} 116 117function fundCmd (args, cb) { 118 const opts = FundConfig(npmConfig()) 119 const dir = path.resolve(npm.dir, '..') 120 const packageName = args[0] 121 const numberArg = opts.which 122 123 const fundingSourceNumber = numberArg && parseInt(numberArg, 10) 124 125 if (numberArg !== undefined && (String(fundingSourceNumber) !== numberArg || fundingSourceNumber < 1)) { 126 const err = new Error('`npm fund [<@scope>/]<pkg> [--which=fundingSourceNumber]` must be given a positive integer') 127 err.code = 'EFUNDNUMBER' 128 throw err 129 } 130 131 if (opts.global) { 132 const err = new Error('`npm fund` does not support global packages') 133 err.code = 'EFUNDGLOBAL' 134 throw err 135 } 136 137 if (packageName) { 138 openFundingUrl(packageName, fundingSourceNumber, cb) 139 return 140 } 141 142 readPackageTree(dir, function (err, tree) { 143 if (err) { 144 process.exitCode = 1 145 return cb(err) 146 } 147 148 readShrinkwrap.andInflate(tree, function () { 149 const fundingInfo = getFundingInfo( 150 mutateIntoLogicalTree.asReadInstalled( 151 computeMetadata(tree) 152 ) 153 ) 154 155 const print = opts.json 156 ? printJSON 157 : printHuman 158 159 output( 160 print( 161 fundingInfo, 162 opts 163 ) 164 ) 165 cb(err, tree) 166 }) 167 }) 168} 169