Skip to content

Commit 266d076

Browse files
kemitchellruyadorno
authored andcommitted
support: add support subcommand
PR-URL: #246 Credit: @kemitchell Close: #246 Reviewed-by: @ruyadorno Thanks @kemitchell for providing the initial work that served as a base for `npm fund`, its original commits messages are preserved as such: - support: add support subcommand - support: fix request caching - support: further sanitize contributor data - doc: Fix typo - support: simplify to just collecting and showing URLs - install: improve `npm support` test - install: drop "the" before "projects you depend on" - doc: Reword mention of `npm support` in `package.json` spec
1 parent cd14d47 commit 266d076

File tree

8 files changed

+259
-10
lines changed

8 files changed

+259
-10
lines changed

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

+10
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,16 @@ 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
198+
199+
You can specify a URL for up-to-date information about ways to support
200+
development of your package:
201+
202+
{ "support": "https://example.com/project/support" }
203+
204+
Users can use the `npm support` subcommand to list the `support` URLs
205+
of all dependencies of the project, direct and indirect.
206+
197207
### files
198208

199209
The optional `files` field is an array of file patterns that describes

lib/config/cmd-list.js

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

9697
'help',

lib/install.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ var unlock = locker.unlock
119119
var parseJSON = require('./utils/parse-json.js')
120120
var output = require('./utils/output.js')
121121
var saveMetrics = require('./utils/metrics.js').save
122+
var validSupportURL = require('./utils/valid-support-url')
122123

123124
// install specific libraries
124125
var copyTree = require('./install/copy-tree.js')
@@ -802,13 +803,20 @@ Installer.prototype.printInstalledForHuman = function (diffs, auditResult) {
802803
var added = 0
803804
var updated = 0
804805
var moved = 0
806+
// Check if any installed packages have support properties.
807+
var haveSupportable = false
805808
// Count the number of contributors to packages added, tracking
806809
// contributors we've seen, so we can produce a running unique count.
807810
var contributors = new Set()
808811
diffs.forEach(function (action) {
809812
var mutation = action[0]
810813
var pkg = action[1]
811814
if (pkg.failed) return
815+
if (
816+
mutation !== 'remove' && validSupportURL(pkg.package.support)
817+
) {
818+
haveSupportable = true
819+
}
812820
if (mutation === 'remove') {
813821
++removed
814822
} else if (mutation === 'move') {
@@ -872,7 +880,12 @@ Installer.prototype.printInstalledForHuman = function (diffs, auditResult) {
872880
report += ' in ' + ((Date.now() - this.started) / 1000) + 's'
873881

874882
output(report)
875-
return auditResult && audit.printInstallReport(auditResult)
883+
if (haveSupportable) {
884+
output('Run `npm support` to support projects you depend on.')
885+
}
886+
if (auditResult) {
887+
audit.printInstallReport(auditResult)
888+
}
876889

877890
function packages (num) {
878891
return num + ' package' + (num > 1 ? 's' : '')

lib/support.js

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
'use strict'
2+
3+
const npm = require('./npm.js')
4+
const output = require('./utils/output.js')
5+
const path = require('path')
6+
const readPackageTree = require('read-package-tree')
7+
const semver = require('semver')
8+
const validSupportURL = require('./utils/valid-support-url')
9+
10+
module.exports = support
11+
12+
const usage = require('./utils/usage')
13+
support.usage = usage(
14+
'support',
15+
'\nnpm support [--json]'
16+
)
17+
18+
support.completion = function (opts, cb) {
19+
const argv = opts.conf.argv.remain
20+
switch (argv[2]) {
21+
case 'support':
22+
return cb(null, [])
23+
default:
24+
return cb(new Error(argv[2] + ' not recognized'))
25+
}
26+
}
27+
28+
// Compare lib/ls.js.
29+
function support (args, silent, cb) {
30+
if (typeof cb !== 'function') {
31+
cb = silent
32+
silent = false
33+
}
34+
const dir = path.resolve(npm.dir, '..')
35+
readPackageTree(dir, function (err, tree) {
36+
if (err) {
37+
process.exitCode = 1
38+
return cb(err)
39+
}
40+
const data = findPackages(tree)
41+
if (silent) return cb(null, data)
42+
var out
43+
if (npm.config.get('json')) {
44+
out = JSON.stringify(data, null, 2)
45+
} else {
46+
out = data.map(displayPackage).join('\n\n')
47+
}
48+
output(out)
49+
cb(err, data)
50+
})
51+
}
52+
53+
function findPackages (root) {
54+
const set = new Set()
55+
iterate(root)
56+
return Array.from(set).sort(function (a, b) {
57+
const comparison = a.name
58+
.toLowerCase()
59+
.localeCompare(b.name.toLowerCase())
60+
return comparison === 0
61+
? semver.compare(a.version, b.version)
62+
: comparison
63+
})
64+
65+
function iterate (node) {
66+
node.children.forEach(recurse)
67+
}
68+
69+
function recurse (node) {
70+
const metadata = node.package
71+
const support = metadata.support
72+
if (support && validSupportURL(support)) {
73+
set.add({
74+
name: metadata.name,
75+
version: metadata.version,
76+
path: node.path,
77+
homepage: metadata.homepage,
78+
repository: metadata.repository,
79+
support: metadata.support
80+
})
81+
}
82+
if (node.children) iterate(node)
83+
}
84+
}
85+
86+
function displayPackage (entry) {
87+
return entry.name + '@' + entry.version + ': ' + entry.support
88+
}

lib/utils/valid-support-url.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const URL = require('url').URL
2+
3+
// Is the value of a `support` property of a `package.json` object
4+
// a valid URL for `npm support` to display?
5+
module.exports = function (argument) {
6+
if (typeof argument !== 'string' || argument.length === 0) {
7+
return false
8+
}
9+
try {
10+
var parsed = new URL(argument)
11+
} catch (error) {
12+
return false
13+
}
14+
if (
15+
parsed.protocol !== 'https:' &&
16+
parsed.protocol !== 'http:'
17+
) return false
18+
return parsed.host
19+
}

package-lock.json

+11-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/tap/install-mention-support.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict'
2+
var test = require('tap').test
3+
var Tacks = require('tacks')
4+
var Dir = Tacks.Dir
5+
var File = Tacks.File
6+
var common = require('../common-tap.js')
7+
8+
var fixturepath = common.pkg
9+
var fixture = new Tacks(Dir({
10+
'package.json': File({}),
11+
'hassupport': Dir({
12+
'package.json': File({
13+
name: 'hassupport',
14+
version: '7.7.7',
15+
support: 'http://example.com/project/support'
16+
})
17+
})
18+
}))
19+
20+
test('setup', function (t) {
21+
fixture.remove(fixturepath)
22+
fixture.create(fixturepath)
23+
t.end()
24+
})
25+
26+
test('install-report', function (t) {
27+
common.npm(['install', '--no-save', './hassupport'], {cwd: fixturepath}, function (err, code, stdout, stderr) {
28+
if (err) throw err
29+
t.is(code, 0, 'installed successfully')
30+
t.is(stderr, '', 'no warnings')
31+
t.includes(stdout, '`npm support`', 'mentions `npm support`')
32+
t.end()
33+
})
34+
})
35+
36+
test('cleanup', function (t) {
37+
fixture.remove(fixturepath)
38+
t.end()
39+
})

test/tap/support.js

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use strict'
2+
var test = require('tap').test
3+
var Tacks = require('tacks')
4+
var path = require('path')
5+
var Dir = Tacks.Dir
6+
var File = Tacks.File
7+
var common = require('../common-tap.js')
8+
9+
var fixturepath = common.pkg
10+
var fixture = new Tacks(Dir({
11+
'package.json': File({
12+
name: 'a',
13+
version: '0.0.0',
14+
dependencies: { 'hassupport': '7.7.7' }
15+
}),
16+
'node_modules': Dir({
17+
hassupport: Dir({
18+
'package.json': File({
19+
name: 'hassupport',
20+
version: '7.7.7',
21+
homepage: 'http://example.com/project',
22+
support: 'http://example.com/project/donate'
23+
})
24+
})
25+
})
26+
}))
27+
28+
test('setup', function (t) {
29+
fixture.remove(fixturepath)
30+
fixture.create(fixturepath)
31+
t.end()
32+
})
33+
34+
test('support --json', function (t) {
35+
common.npm(['support', '--json'], {cwd: fixturepath}, function (err, code, stdout, stderr) {
36+
if (err) throw err
37+
t.is(code, 0, 'exited 0')
38+
t.is(stderr, '', 'no warnings')
39+
var parsed
40+
t.doesNotThrow(function () {
41+
parsed = JSON.parse(stdout)
42+
}, 'valid JSON')
43+
t.deepEqual(
44+
parsed,
45+
[
46+
{
47+
name: 'hassupport',
48+
version: '7.7.7',
49+
homepage: 'http://example.com/project',
50+
support: 'http://example.com/project/donate',
51+
path: path.resolve(fixturepath, 'node_modules', 'hassupport')
52+
}
53+
],
54+
'output data'
55+
)
56+
t.end()
57+
})
58+
})
59+
60+
test('support', function (t) {
61+
common.npm(['support'], {cwd: fixturepath}, function (err, code, stdout, stderr) {
62+
if (err) throw err
63+
t.is(code, 0, 'exited 0')
64+
t.is(stderr, '', 'no warnings')
65+
t.includes(stdout, 'hassupport', 'outputs project name')
66+
t.includes(stdout, '7.7.7', 'outputs project version')
67+
t.includes(stdout, 'http://example.com/project', 'outputs contributor homepage')
68+
t.includes(stdout, 'http://example.com/project/donate', 'outputs support link')
69+
t.end()
70+
})
71+
})
72+
73+
test('cleanup', function (t) {
74+
t.pass(fixturepath)
75+
fixture.remove(fixturepath)
76+
t.end()
77+
})

0 commit comments

Comments
 (0)