Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
fund.js216 linesDownload Raw Back to commands
1const archy = require('archy')
2const pacote = require('pacote')
3const semver = require('semver')
4const { output } = require('proc-log')
5const npa = require('npm-package-arg')
6const { depth } = require('treeverse')
7const { readTree: getFundingInfo, normalizeFunding, isValidFunding } = require('libnpmfund')
8const { openUrl } = require('../utils/open-url.js')
9const ArboristWorkspaceCmd = require('../arborist-cmd.js')
10
11const getPrintableName = ({ name, version }) => {
12  const printableVersion = version ? `@${version}` : ''
13  return `${name}${printableVersion}`
14}
15
16const errCode = (msg, code) => Object.assign(new Error(msg), { code })
17
18class Fund extends ArboristWorkspaceCmd {
19  static description = 'Retrieve funding information'
20  static name = 'fund'
21  static params = ['json', 'browser', 'unicode', 'workspace', 'which']
22  static usage = ['[<package-spec>]']
23
24  // XXX: maybe worth making this generic for all commands?
25  usageMessage (paramsObj = {}) {
26    let msg = `\`npm ${this.constructor.name}`
27    const params = Object.entries(paramsObj)
28    if (params.length) {
29      msg += ` ${this.constructor.usage}`
30    }
31    for (const [key, value] of params) {
32      msg += ` --${key}=${value}`
33    }
34    return `${msg}\``
35  }
36
37  static async completion (opts, npm) {
38    const completion = require('../utils/installed-deep.js')
39    return completion(npm, opts)
40  }
41
42  async exec (args) {
43    const spec = args[0]
44
45    let fundingSourceNumber = this.npm.config.get('which')
46    if (fundingSourceNumber != null) {
47      fundingSourceNumber = parseInt(fundingSourceNumber, 10)
48      if (isNaN(fundingSourceNumber) || fundingSourceNumber < 1) {
49        throw errCode(
50          `${this.usageMessage({ which: 'fundingSourceNumber' })} must be given a positive integer`,
51          'EFUNDNUMBER'
52        )
53      }
54    }
55
56    if (this.npm.global) {
57      throw errCode(
58        `${this.usageMessage()} does not support global packages`,
59        'EFUNDGLOBAL'
60      )
61    }
62
63    const where = this.npm.prefix
64    const Arborist = require('@npmcli/arborist')
65    const arb = new Arborist({ ...this.npm.flatOptions, path: where })
66    const tree = await arb.loadActual()
67
68    if (spec) {
69      await this.openFundingUrl({
70        path: where,
71        tree,
72        spec,
73        fundingSourceNumber,
74      })
75      return
76    }
77
78    // TODO: add !workspacesEnabled option handling to libnpmfund
79    const fundingInfo = getFundingInfo(tree, {
80      ...this.flatOptions,
81      Arborist,
82      workspaces: this.workspaceNames,
83    })
84
85    if (this.npm.config.get('json')) {
86      output.buffer(fundingInfo)
87    } else {
88      output.standard(this.printHuman(fundingInfo))
89    }
90  }
91
92  printHuman (fundingInfo) {
93    const unicode = this.npm.config.get('unicode')
94    const seenUrls = new Map()
95
96    const tree = obj => archy(obj, '', { unicode })
97
98    const result = depth({
99      tree: fundingInfo,
100
101      // composes human readable package name and creates a new archy item for readable output
102      visit: ({ name, version, funding }) => {
103        const [fundingSource] = [].concat(normalizeFunding(funding)).filter(isValidFunding)
104        const { url } = fundingSource || {}
105        const pkgRef = getPrintableName({ name, version })
106
107        if (!url) {
108          return { label: pkgRef }
109        }
110        let item
111        if (seenUrls.has(url)) {
112          item = seenUrls.get(url)
113          item.label += `${this.npm.chalk.dim(',')} ${pkgRef}`
114          return null
115        }
116        item = {
117          label: tree({
118            label: this.npm.chalk.blue(url),
119            nodes: [pkgRef],
120          }).trim(),
121        }
122
123        // stacks all packages together under the same item
124        seenUrls.set(url, item)
125        return item
126      },
127
128      // puts child nodes back into returned archy output while also filtering out missing items
129      leave: (item, children) => {
130        if (item) {
131          item.nodes = children.filter(Boolean)
132        }
133
134        return item
135      },
136
137      // turns tree-like object return by libnpmfund into children to be properly read by treeverse
138      getChildren: node =>
139        Object.keys(node.dependencies || {}).map(key => ({
140          name: key,
141          ...node.dependencies[key],
142        })),
143    })
144
145    const res = tree(result)
146    return res
147  }
148
149  async openFundingUrl ({ path, tree, spec, fundingSourceNumber }) {
150    const arg = npa(spec, path)
151
152    const retrievePackageMetadata = () => {
153      if (arg.type === 'directory') {
154        if (tree.path === arg.fetchSpec) {
155          // matches cwd, e.g: npm fund .
156          return tree.package
157        } else {
158          // matches any file path within current arborist inventory
159          for (const item of tree.inventory.values()) {
160            if (item.path === arg.fetchSpec) {
161              return item.package
162            }
163          }
164        }
165      } else {
166        // tries to retrieve a package from arborist inventory by matching resulted package name from the provided spec
167        const [item] = [...tree.inventory.query('name', arg.name)]
168          .filter(i => semver.valid(i.package.version))
169          .sort((a, b) => semver.rcompare(a.package.version, b.package.version))
170
171        if (item) {
172          return item.package
173        }
174      }
175    }
176
177    const { funding } =
178      retrievePackageMetadata() ||
179      (await pacote.manifest(arg, this.npm.flatOptions).catch(() => ({})))
180
181    const validSources = [].concat(normalizeFunding(funding)).filter(isValidFunding)
182
183    if (!validSources.length) {
184      throw errCode(`No valid funding method available for: ${spec}`, 'ENOFUND')
185    }
186
187    const fundSource = fundingSourceNumber
188      ? validSources[fundingSourceNumber - 1]
189      : validSources.length === 1 ? validSources[0]
190      : null
191
192    if (fundSource) {
193      return openUrl(this.npm, ...this.urlMessage(fundSource))
194    }
195
196    const ambiguousUrlMsg = [
197      ...validSources.map((s, i) => `${i + 1}: ${this.urlMessage(s).reverse().join(': ')}`),
198      `Run ${this.usageMessage({ which: '1' })}` +
199      ', for example, to open the first funding URL listed in that package',
200    ]
201    if (fundingSourceNumber) {
202      ambiguousUrlMsg.unshift(`--which=${fundingSourceNumber} is not a valid index`)
203    }
204    output.standard(ambiguousUrlMsg.join('\n'))
205  }
206
207  urlMessage (source) {
208    const { type, url } = source
209    const typePrefix = type ? `${type} funding` : 'Funding'
210    const message = `${typePrefix} available at the following URL`
211    return [url, message]
212  }
213}
214
215module.exports = Fund
216 
codekingpro/portable-devtools · Team Ai