Skip to content

Commit a8a2001

Browse files
committed
support: add support subcommand
1 parent b4ff454 commit a8a2001

9 files changed

Lines changed: 490 additions & 8 deletions

File tree

doc/files/package.json.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,25 @@ Both email and url are optional either way.
170170

171171
npm also sets a top-level "maintainers" field with your npm user info.
172172

173+
## support
174+
175+
You can specify an HTTP endpoint for up-to-date information about ways
176+
to support development of your package:
177+
178+
{ "support": "https://example.com/support.json" }
179+
180+
For example, you might like to develop your support data file in your
181+
source code repository:
182+
183+
{ "support": "https://raw.githubusercontent.com/{user}/{repo}/master/support.json" }
184+
185+
The URL you specify should respond to unauthenticated GET requests
186+
with a JSON object. If the JSON object contains a `contributors`
187+
array, `npm support` will interpret it as a `support.json` file.
188+
If the JSON object contains a `versions` array, `npm support`
189+
will interpret it as [Node.js Package Maintenance Working
190+
Group](https://github.com/nodejs/package-maintenance) metadata.
191+
173192
## files
174193

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

doc/files/support.json

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
spuport.json(5) -- Specifics of npm's support.json handling
2+
===========================================================
3+
4+
## DESCRIPTION
5+
6+
This document describes the format of `support.json` files, which you
7+
can use to share information about how to support your work and projects
8+
through `npm support`.
9+
10+
`support.json` data must be actual JSON, not just a JavaScript object
11+
literal.
12+
13+
## contributors
14+
15+
Each `support.json` file must contain a `contributors` property whose
16+
value is an array. That array can contain two types of objects.
17+
18+
Contributor objects provide information about people and organizations
19+
that produce a package, and how to suppor them. For example:
20+
21+
```json
22+
{
23+
"name": "Ana Exemplar",
24+
"homepage": "http://example.com/anaexemplar",
25+
"links": [
26+
"http://patreon.com/anaexemplar"
27+
]
28+
}
29+
```
30+
31+
```json
32+
{
33+
"name": "Ana Exemplar",
34+
"homepage": "http://example.com/anaexemplar",
35+
"links": [
36+
"http://patreon.com/anaexemplar"
37+
]
38+
}
39+
40+
```json
41+
{
42+
"name": "JS Foundation",
43+
"type": "organization",
44+
"homepage": "https://js.foundation",
45+
"links": [
46+
"https://js.foundation/about/donate"
47+
]
48+
}
49+
```
50+
51+
`contributors` array items may also include URLs for contributor objects:
52+
53+
```json
54+
{
55+
"url": "http://example.com/support-anaexemplar.json"
56+
}
57+
```

lib/config/cmd-list.js

Lines changed: 1 addition & 0 deletions
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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -802,13 +802,18 @@ Installer.prototype.printInstalledForHuman = function (diffs, auditResult) {
802802
var added = 0
803803
var updated = 0
804804
var moved = 0
805+
// Check if any installed packages have support properties.
806+
var haveSupportable = false
805807
// Count the number of contributors to packages added, tracking
806808
// contributors we've seen, so we can produce a running unique count.
807809
var contributors = new Set()
808810
diffs.forEach(function (action) {
809811
var mutation = action[0]
810812
var pkg = action[1]
811813
if (pkg.failed) return
814+
if (mutation !== 'remove' && pkg.package.support) {
815+
haveSupportable = true
816+
}
812817
if (mutation === 'remove') {
813818
++removed
814819
} else if (mutation === 'move') {
@@ -872,7 +877,12 @@ Installer.prototype.printInstalledForHuman = function (diffs, auditResult) {
872877
report += ' in ' + ((Date.now() - this.started) / 1000) + 's'
873878

874879
output(report)
875-
return auditResult && audit.printInstallReport(auditResult)
880+
if (haveSupportable) {
881+
output('Run `npm support` to support the projects you depend on.')
882+
}
883+
if (auditResult) {
884+
audit.printInstallReport(auditResult)
885+
}
876886

877887
function packages (num) {
878888
return num + ' package' + (num > 1 ? 's' : '')

lib/support.js

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
'use strict'
2+
3+
var npm = require('./npm.js')
4+
var output = require('./utils/output.js')
5+
var readPackageTree = require('read-package-tree')
6+
var runParallelLimit = require('run-parallel-limit')
7+
var simpleGet = require('simple-get')
8+
var semver = require('semver')
9+
var hasANSI = require('has-ansi')
10+
11+
module.exports = support
12+
13+
const usage = require('./utils/usage')
14+
support.usage = usage(
15+
'support',
16+
'\nnpm support [--json]'
17+
)
18+
19+
support.completion = function (opts, cb) {
20+
const argv = opts.conf.argv.remain
21+
22+
switch (argv[2]) {
23+
case 'support':
24+
return cb(null, [])
25+
default:
26+
return cb(new Error(argv[2] + ' not recognized'))
27+
}
28+
}
29+
30+
function support (args, silent, cb) {
31+
readPackageTree(npm.dir, function (error, tree) {
32+
if (error) return cb(error)
33+
var supportablePackages = Array.from(findSupportablePackages(tree))
34+
downloadSupportData(supportablePackages, function (error, data) {
35+
if (error) return cb(error)
36+
37+
if (typeof cb !== 'function') {
38+
cb = silent
39+
silent = false
40+
}
41+
if (silent) return cb(null, data)
42+
43+
var out
44+
var json = npm.config.get('json')
45+
if (json) {
46+
out = JSON.stringify(data, null, 2)
47+
} else {
48+
out = data
49+
.sort(function (a, b) {
50+
var comparison = a.name.localeCompare(b.name)
51+
return comparison === 0
52+
? semver.compare(a.version, b.version)
53+
: comparison
54+
})
55+
.map(displaySupportData)
56+
.join('\n\n')
57+
}
58+
output(out)
59+
if (error) process.exitCode = 1
60+
cb(error, data)
61+
})
62+
})
63+
}
64+
65+
function findSupportablePackages (root) {
66+
var set = new Set()
67+
iterate(root)
68+
return set
69+
70+
function iterate (node) {
71+
node.children.forEach(recurse)
72+
}
73+
74+
function recurse (node) {
75+
var metadata = node.package
76+
if (metadata.support) {
77+
set.add({
78+
name: metadata.name,
79+
version: metadata.version,
80+
homepage: metadata.homepage,
81+
repository: metadata.repository,
82+
support: metadata.support,
83+
parent: node.parent,
84+
path: node.path
85+
})
86+
}
87+
if (node.children) iterate(node)
88+
}
89+
}
90+
91+
function downloadSupportData (supportablePackages, cb) {
92+
var cache = new Map()
93+
var headers = { 'user-agent': npm.config.get('user-agent') }
94+
runParallelLimit(supportablePackages.map(function (entry) {
95+
return function task (done) {
96+
var url = entry.support
97+
get(url, function (error, response, projectData) {
98+
if (error) {
99+
return done(null, {
100+
url: url,
101+
error: 'could not download data'
102+
})
103+
}
104+
if (typeof projectData !== 'object' || Array.isArray(projectData)) {
105+
return done(null, {
106+
url: url,
107+
error: 'not an object'
108+
})
109+
}
110+
var contributors = projectData.contributors
111+
if (!Array.isArray(contributors)) {
112+
return done(null, projectData)
113+
}
114+
runParallelLimit(contributors.map(function (contributor) {
115+
return function (done) {
116+
if (
117+
typeof contributor !== 'object' ||
118+
typeof contributor.url !== 'string'
119+
) {
120+
return setImmediate(function () {
121+
done(null, contributor)
122+
})
123+
}
124+
get(contributor.url, function (error, response, contributorData) {
125+
if (error) {
126+
return done(null, {
127+
url: contributor.url,
128+
error: error
129+
})
130+
}
131+
contributorData.url = contributor.url
132+
done(null, contributorData)
133+
})
134+
}
135+
}), 5, function (error, resolvedContributors) {
136+
if (error) return done(error)
137+
done(null, {
138+
name: entry.name,
139+
version: entry.version,
140+
url: entry.support,
141+
homepage: entry.homepage,
142+
contributors: resolvedContributors
143+
})
144+
})
145+
})
146+
}
147+
}), 5, cb)
148+
149+
function get (url, cb) {
150+
var cached = cache.get(url)
151+
if (cached) {
152+
return setImmediate(function () {
153+
cb(null, {cached: true}, cached)
154+
})
155+
}
156+
simpleGet.concat({
157+
url: url,
158+
json: true,
159+
headers: headers
160+
}, cb)
161+
}
162+
}
163+
164+
function displaySupportData (entry) {
165+
var returned = [entry.name + '@' + entry.version]
166+
if (looksLikeURL(entry.homepage)) {
167+
returned[0] += ' (' + entry.homepage + ')'
168+
}
169+
if (Array.isArray(entry.contributors)) {
170+
entry.contributors.forEach(function (contributor) {
171+
var name = contributor.name
172+
if (looksLikeSafeString(name)) {
173+
var item = ['- ' + name]
174+
var email = contributor.email
175+
if (looksLikeSafeString(email)) {
176+
item[0] += ' <' + email + '>'
177+
}
178+
var homepage = contributor.homepage
179+
if (looksLikeURL(homepage)) {
180+
item[0] += ' (' + homepage + ')'
181+
}
182+
var links = contributor.links
183+
if (Array.isArray(links)) {
184+
links.forEach(function (link) {
185+
if (looksLikeURL(link)) item.push(' ' + link)
186+
})
187+
}
188+
returned.push(item.join('\n'))
189+
}
190+
})
191+
}
192+
return returned.join('\n')
193+
}
194+
195+
function looksLikeSafeString (argument) {
196+
return (
197+
typeof argument === 'string' &&
198+
argument.length > 0 &&
199+
argument.length < 80 &&
200+
!hasANSI(argument)
201+
)
202+
}
203+
204+
function looksLikeURL (argument) {
205+
return (
206+
looksLikeSafeString(argument) &&
207+
(
208+
argument.indexOf('https://') === 0 ||
209+
argument.indexOf('http://') === 0
210+
)
211+
)
212+
}

0 commit comments

Comments
 (0)