• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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