codekingpro/portable-devtools
114k
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 