Skip to content

Commit 4414b06

Browse files
committed
fund: add fund command
This commit introduces the `npm fund` command that lists all `funding` info provided by the installed dependencies of a given project. Notes on implementation: - `lib/utils/funding.js` Provides helpers to validate funding info and return a tree-shaped structure containing the funding data for all deps. - `lib/fund.js` Implements `npm fund <pkg>` command - Added tests - `npm install` mention of funding - `npm fund <pkg>` variations - unit tests for added `lib/utils` and `lib/install` helpers - Added docs for `npm fund`, `funding` `package.json` property - Fixed `lib/utils/open-url` to support `--json` config - Documented `unicode` on `npm install` docs - fix tests - fix planned tap tests - alternative solution to --no-browser arg - docs: moved fund docs to new location Refs: https://github.com/npm/rfcs/blob/2d2f00457ab19b3003eb6ac5ab3d250259fd5a81/accepted/0017-add-funding-support.md PR-URL: #273 Credit: @ruyadorno Close: #273 Reviewed-by: @darcyclarke
1 parent 266d076 commit 4414b06

25 files changed

+1663
-255
lines changed

docs/content/cli-commands/npm-audit.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ description: Run a security audit
88

99
## Run a security audit
1010

11-
### Synposis
11+
### Synopsis
1212

1313
```bash
1414
npm audit [--json|--parseable|--audit-level=(low|moderate|high|critical)]

docs/content/cli-commands/npm-fund.md

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
section: cli-commands
3+
title: npm-fund
4+
description: Retrieve funding information
5+
---
6+
7+
# npm-fund
8+
9+
## Retrieve funding information
10+
11+
### Synopsis
12+
13+
```bash
14+
npm fund [<pkg>]
15+
```
16+
17+
### Description
18+
19+
This command retrieves information on how to fund the dependencies of
20+
a given project. If no package name is provided, it will list all
21+
dependencies that are looking for funding in a tree-structure in which
22+
are listed the type of funding and the url to visit. If a package name
23+
is provided then it tries to open its funding url using the `--browser`
24+
config param.
25+
26+
The list will avoid duplicated entries and will stack all packages
27+
that share the same type/url as a single entry. Given this nature the
28+
list is not going to have the same shape of the output from `npm ls`.
29+
30+
### Configuration
31+
32+
#### browser
33+
34+
* Default: OS X: `"open"`, Windows: `"start"`, Others: `"xdg-open"`
35+
* Type: String
36+
37+
The browser that is called by the `npm fund` command to open websites.
38+
39+
#### json
40+
41+
* Default: false
42+
* Type: Boolean
43+
44+
Show information in JSON format.
45+
46+
#### unicode
47+
48+
* Type: Boolean
49+
* Default: true
50+
51+
Whether to represent the tree structure using unicode characters.
52+
Set it to `false` in order to use all-ansi output.
53+
54+
## See Also
55+
56+
* [npm-docs](/cli-commands/npm-docs)
57+
* [npm-config](/cli-commands/npm-config)
58+
* [npm-install](/cli-commands/npm-install)
59+
* [npm-ls](/cli-commands/npm-ls)
60+

docs/content/cli-commands/npm-install.md

+5
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,10 @@ local copy exists on disk.
357357
npm install sax --force
358358
```
359359
360+
The `--no-fund` argument will hide the message displayed at the end of each
361+
install that aknowledges the number of dependencies looking for funding.
362+
See `npm-fund(1)`
363+
360364
The `-g` or `--global` argument will cause npm to install the package globally
361365
rather than locally. See [npm-folders](/docs/configuring-npm/folders).
362366
@@ -481,6 +485,7 @@ affects a real use-case, it will be investigated.
481485
* [npm folders](/configuring-npm/folders)
482486
* [npm update](/cli-commands/npm-update)
483487
* [npm audit](/cli-commands/npm-audit)
488+
* [npm fund](/cli-commands/npm-fund)
484489
* [npm link](/cli-commands/npm-link)
485490
* [npm rebuild](/cli-commands/npm-rebuild)
486491
* [npm scripts](/using-npm/scripts)

docs/content/cli-commands/npm-ls.md

+8
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ When "prod" or "production", is an alias to `production`.
109109

110110
Display only dependencies which are linked
111111

112+
#### unicode
113+
114+
* Type: Boolean
115+
* Default: true
116+
117+
Whether to represent the tree structure using unicode characters.
118+
Set it to false in order to use all-ansi output.
119+
112120
### See Also
113121

114122
* [npm config](/cli-commands/npm-config)

docs/content/configuring-npm/package-json.md

+16-6
Original file line numberDiff line numberDiff line change
@@ -194,15 +194,25 @@ Both email and url are optional either way.
194194

195195
npm also sets a top-level "maintainers" field with your npm user info.
196196

197-
### support
197+
### funding
198198

199-
You can specify a URL for up-to-date information about ways to support
200-
development of your package:
199+
You can specify an object containing an URL that provides up-to-date
200+
information about ways to help fund development of your package:
201201

202-
{ "support": "https://example.com/project/support" }
202+
"funding": {
203+
"type" : "individual",
204+
"url" : "http://example.com/donate"
205+
}
206+
207+
"funding": {
208+
"type" : "patreon",
209+
"url" : "https://www.patreon.com/my-account"
210+
}
203211

204-
Users can use the `npm support` subcommand to list the `support` URLs
205-
of all dependencies of the project, direct and indirect.
212+
Users can use the `npm fund` subcommand to list the `funding` URLs of all
213+
dependencies of their project, direct and indirect. A shortcut to visit each
214+
funding url is also available when providing the project name such as:
215+
`npm fund <projectname>`.
206216

207217
### files
208218

docs/content/using-npm/config.md

+9
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,15 @@ packages.
449449
The "maxTimeout" config for the `retry` module to use when fetching
450450
packages.
451451

452+
#### fund
453+
454+
* Default: true
455+
* Type: Boolean
456+
457+
When "true" displays the message at the end of each `npm install`
458+
aknowledging the number of dependencies looking for funding.
459+
See [`npm-fund`](/docs/cli-commands/npm-fund) for details.
460+
452461
#### git
453462

454463
* Default: `"git"`

lib/config/cmd-list.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ var cmdList = [
9191
'token',
9292
'profile',
9393
'audit',
94-
'support',
94+
'fund',
9595
'org',
9696

9797
'help',

lib/config/defaults.js

+3
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ Object.defineProperty(exports, 'defaults', {get: function () {
143143
force: false,
144144
'format-package-lock': true,
145145

146+
fund: true,
147+
146148
'fetch-retries': 2,
147149
'fetch-retry-factor': 10,
148150
'fetch-retry-mintimeout': 10000,
@@ -284,6 +286,7 @@ exports.types = {
284286
editor: String,
285287
'engine-strict': Boolean,
286288
force: Boolean,
289+
fund: Boolean,
287290
'format-package-lock': Boolean,
288291
'fetch-retries': Number,
289292
'fetch-retry-factor': Number,

lib/fund.js

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
'use strict'
2+
3+
const path = require('path')
4+
5+
const archy = require('archy')
6+
const figgyPudding = require('figgy-pudding')
7+
const readPackageTree = require('read-package-tree')
8+
9+
const npm = require('./npm.js')
10+
const npmConfig = require('./config/figgy-config.js')
11+
const fetchPackageMetadata = require('./fetch-package-metadata.js')
12+
const computeMetadata = require('./install/deps.js').computeMetadata
13+
const readShrinkwrap = require('./install/read-shrinkwrap.js')
14+
const mutateIntoLogicalTree = require('./install/mutate-into-logical-tree.js')
15+
const output = require('./utils/output.js')
16+
const openUrl = require('./utils/open-url.js')
17+
const { getFundingInfo, validFundingUrl } = require('./utils/funding.js')
18+
19+
const FundConfig = figgyPudding({
20+
browser: {}, // used by ./utils/open-url
21+
global: {},
22+
json: {},
23+
unicode: {}
24+
})
25+
26+
module.exports = fundCmd
27+
28+
const usage = require('./utils/usage')
29+
fundCmd.usage = usage(
30+
'fund',
31+
'npm fund [--json]',
32+
'npm fund [--browser] [[<@scope>/]<pkg>'
33+
)
34+
35+
fundCmd.completion = function (opts, cb) {
36+
const argv = opts.conf.argv.remain
37+
switch (argv[2]) {
38+
case 'fund':
39+
return cb(null, [])
40+
default:
41+
return cb(new Error(argv[2] + ' not recognized'))
42+
}
43+
}
44+
45+
function printJSON (fundingInfo) {
46+
return JSON.stringify(fundingInfo, null, 2)
47+
}
48+
49+
// the human-printable version does some special things that turned out to
50+
// be very verbose but hopefully not hard to follow: we stack up items
51+
// that have a shared url/type and make sure they're printed at the highest
52+
// level possible, in that process they also carry their dependencies along
53+
// with them, moving those up in the visual tree
54+
function printHuman (fundingInfo, opts) {
55+
// mapping logic that keeps track of seen items in order to be able
56+
// to push all other items from the same type/url in the same place
57+
const seen = new Map()
58+
59+
function seenKey ({ type, url } = {}) {
60+
return url ? String(type) + String(url) : null
61+
}
62+
63+
function setStackedItem (funding, result) {
64+
const key = seenKey(funding)
65+
if (key && !seen.has(key)) seen.set(key, result)
66+
}
67+
68+
function retrieveStackedItem (funding) {
69+
const key = seenKey(funding)
70+
if (key && seen.has(key)) return seen.get(key)
71+
}
72+
73+
// ---
74+
75+
const getFundingItems = (fundingItems) =>
76+
Object.keys(fundingItems || {}).map((fundingItemName) => {
77+
// first-level loop, prepare the pretty-printed formatted data
78+
const fundingItem = fundingItems[fundingItemName]
79+
const { version, funding } = fundingItem
80+
const { type, url } = funding || {}
81+
82+
const printableVersion = version ? `@${version}` : ''
83+
const printableType = type && { label: `type: ${funding.type}` }
84+
const printableUrl = url && { label: `url: ${funding.url}` }
85+
const result = {
86+
fundingItem,
87+
label: fundingItemName + printableVersion,
88+
nodes: []
89+
}
90+
91+
if (printableType) {
92+
result.nodes.push(printableType)
93+
}
94+
95+
if (printableUrl) {
96+
result.nodes.push(printableUrl)
97+
}
98+
99+
setStackedItem(funding, result)
100+
101+
return result
102+
}).reduce((res, result) => {
103+
// recurse and exclude nodes that are going to be stacked together
104+
const { fundingItem } = result
105+
const { dependencies, funding } = fundingItem
106+
const items = getFundingItems(dependencies)
107+
const stackedResult = retrieveStackedItem(funding)
108+
items.forEach(i => result.nodes.push(i))
109+
110+
if (stackedResult && stackedResult !== result) {
111+
stackedResult.label += `, ${result.label}`
112+
items.forEach(i => stackedResult.nodes.push(i))
113+
return res
114+
}
115+
116+
res.push(result)
117+
118+
return res
119+
}, [])
120+
121+
const [ result ] = getFundingItems({
122+
[fundingInfo.name]: {
123+
dependencies: fundingInfo.dependencies,
124+
funding: fundingInfo.funding,
125+
version: fundingInfo.version
126+
}
127+
})
128+
129+
return archy(result, '', { unicode: opts.unicode })
130+
}
131+
132+
function openFundingUrl (packageName, cb) {
133+
function getUrlAndOpen (packageMetadata) {
134+
const { funding } = packageMetadata
135+
const { type, url } = funding || {}
136+
const noFundingError =
137+
new Error(`No funding method available for: ${packageName}`)
138+
noFundingError.code = 'ENOFUND'
139+
const typePrefix = type ? `${type} funding` : 'Funding'
140+
const msg = `${typePrefix} available at the following URL`
141+
142+
if (validFundingUrl(funding)) {
143+
openUrl(url, msg, cb)
144+
} else {
145+
throw noFundingError
146+
}
147+
}
148+
149+
fetchPackageMetadata(
150+
packageName,
151+
'.',
152+
{ fullMetadata: true },
153+
function (err, packageMetadata) {
154+
if (err) return cb(err)
155+
getUrlAndOpen(packageMetadata)
156+
}
157+
)
158+
}
159+
160+
function fundCmd (args, cb) {
161+
const opts = FundConfig(npmConfig())
162+
const dir = path.resolve(npm.dir, '..')
163+
const packageName = args[0]
164+
165+
if (opts.global) {
166+
const err = new Error('`npm fund` does not support globals')
167+
err.code = 'EFUNDGLOBAL'
168+
throw err
169+
}
170+
171+
if (packageName) {
172+
openFundingUrl(packageName, cb)
173+
return
174+
}
175+
176+
readPackageTree(dir, function (err, tree) {
177+
if (err) {
178+
process.exitCode = 1
179+
return cb(err)
180+
}
181+
182+
readShrinkwrap.andInflate(tree, function () {
183+
const fundingInfo = getFundingInfo(
184+
mutateIntoLogicalTree.asReadInstalled(
185+
computeMetadata(tree)
186+
)
187+
)
188+
189+
const print = opts.json
190+
? printJSON
191+
: printHuman
192+
193+
output(
194+
print(
195+
fundingInfo,
196+
opts
197+
)
198+
)
199+
cb(err, tree)
200+
})
201+
})
202+
}

0 commit comments

Comments
 (0)