Skip to content

Commit

Permalink
support: add support subcommand
Browse files Browse the repository at this point in the history
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
  • Loading branch information
kemitchell authored and ruyadorno committed Nov 5, 2019
1 parent cd14d47 commit 266d076
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 10 deletions.
10 changes: 10 additions & 0 deletions docs/content/configuring-npm/package-json.md
Expand Up @@ -194,6 +194,16 @@ Both email and url are optional either way.

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

### support

You can specify a URL for up-to-date information about ways to support
development of your package:

{ "support": "https://example.com/project/support" }

Users can use the `npm support` subcommand to list the `support` URLs
of all dependencies of the project, direct and indirect.

### files

The optional `files` field is an array of file patterns that describes
Expand Down
1 change: 1 addition & 0 deletions lib/config/cmd-list.js
Expand Up @@ -91,6 +91,7 @@ var cmdList = [
'token',
'profile',
'audit',
'support',
'org',

'help',
Expand Down
15 changes: 14 additions & 1 deletion lib/install.js
Expand Up @@ -119,6 +119,7 @@ var unlock = locker.unlock
var parseJSON = require('./utils/parse-json.js')
var output = require('./utils/output.js')
var saveMetrics = require('./utils/metrics.js').save
var validSupportURL = require('./utils/valid-support-url')

// install specific libraries
var copyTree = require('./install/copy-tree.js')
Expand Down Expand Up @@ -802,13 +803,20 @@ Installer.prototype.printInstalledForHuman = function (diffs, auditResult) {
var added = 0
var updated = 0
var moved = 0
// Check if any installed packages have support properties.
var haveSupportable = false
// Count the number of contributors to packages added, tracking
// contributors we've seen, so we can produce a running unique count.
var contributors = new Set()
diffs.forEach(function (action) {
var mutation = action[0]
var pkg = action[1]
if (pkg.failed) return
if (
mutation !== 'remove' && validSupportURL(pkg.package.support)
) {
haveSupportable = true
}
if (mutation === 'remove') {
++removed
} else if (mutation === 'move') {
Expand Down Expand Up @@ -872,7 +880,12 @@ Installer.prototype.printInstalledForHuman = function (diffs, auditResult) {
report += ' in ' + ((Date.now() - this.started) / 1000) + 's'

output(report)
return auditResult && audit.printInstallReport(auditResult)
if (haveSupportable) {
output('Run `npm support` to support projects you depend on.')
}
if (auditResult) {
audit.printInstallReport(auditResult)
}

function packages (num) {
return num + ' package' + (num > 1 ? 's' : '')
Expand Down
88 changes: 88 additions & 0 deletions lib/support.js
@@ -0,0 +1,88 @@
'use strict'

const npm = require('./npm.js')
const output = require('./utils/output.js')
const path = require('path')
const readPackageTree = require('read-package-tree')
const semver = require('semver')
const validSupportURL = require('./utils/valid-support-url')

module.exports = support

const usage = require('./utils/usage')
support.usage = usage(
'support',
'\nnpm support [--json]'
)

support.completion = function (opts, cb) {
const argv = opts.conf.argv.remain
switch (argv[2]) {
case 'support':
return cb(null, [])
default:
return cb(new Error(argv[2] + ' not recognized'))
}
}

// Compare lib/ls.js.
function support (args, silent, cb) {
if (typeof cb !== 'function') {
cb = silent
silent = false
}
const dir = path.resolve(npm.dir, '..')
readPackageTree(dir, function (err, tree) {
if (err) {
process.exitCode = 1
return cb(err)
}
const data = findPackages(tree)
if (silent) return cb(null, data)
var out
if (npm.config.get('json')) {
out = JSON.stringify(data, null, 2)
} else {
out = data.map(displayPackage).join('\n\n')
}
output(out)
cb(err, data)
})
}

function findPackages (root) {
const set = new Set()
iterate(root)
return Array.from(set).sort(function (a, b) {
const comparison = a.name
.toLowerCase()
.localeCompare(b.name.toLowerCase())
return comparison === 0
? semver.compare(a.version, b.version)
: comparison
})

function iterate (node) {
node.children.forEach(recurse)
}

function recurse (node) {
const metadata = node.package
const support = metadata.support
if (support && validSupportURL(support)) {
set.add({
name: metadata.name,
version: metadata.version,
path: node.path,
homepage: metadata.homepage,
repository: metadata.repository,
support: metadata.support
})
}
if (node.children) iterate(node)
}
}

function displayPackage (entry) {
return entry.name + '@' + entry.version + ': ' + entry.support
}
19 changes: 19 additions & 0 deletions lib/utils/valid-support-url.js
@@ -0,0 +1,19 @@
const URL = require('url').URL

// Is the value of a `support` property of a `package.json` object
// a valid URL for `npm support` to display?
module.exports = function (argument) {
if (typeof argument !== 'string' || argument.length === 0) {
return false
}
try {
var parsed = new URL(argument)
} catch (error) {
return false
}
if (
parsed.protocol !== 'https:' &&
parsed.protocol !== 'http:'
) return false
return parsed.host
}
20 changes: 11 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions test/tap/install-mention-support.js
@@ -0,0 +1,39 @@
'use strict'
var test = require('tap').test
var Tacks = require('tacks')
var Dir = Tacks.Dir
var File = Tacks.File
var common = require('../common-tap.js')

var fixturepath = common.pkg
var fixture = new Tacks(Dir({
'package.json': File({}),
'hassupport': Dir({
'package.json': File({
name: 'hassupport',
version: '7.7.7',
support: 'http://example.com/project/support'
})
})
}))

test('setup', function (t) {
fixture.remove(fixturepath)
fixture.create(fixturepath)
t.end()
})

test('install-report', function (t) {
common.npm(['install', '--no-save', './hassupport'], {cwd: fixturepath}, function (err, code, stdout, stderr) {
if (err) throw err
t.is(code, 0, 'installed successfully')
t.is(stderr, '', 'no warnings')
t.includes(stdout, '`npm support`', 'mentions `npm support`')
t.end()
})
})

test('cleanup', function (t) {
fixture.remove(fixturepath)
t.end()
})
77 changes: 77 additions & 0 deletions test/tap/support.js
@@ -0,0 +1,77 @@
'use strict'
var test = require('tap').test
var Tacks = require('tacks')
var path = require('path')
var Dir = Tacks.Dir
var File = Tacks.File
var common = require('../common-tap.js')

var fixturepath = common.pkg
var fixture = new Tacks(Dir({
'package.json': File({
name: 'a',
version: '0.0.0',
dependencies: { 'hassupport': '7.7.7' }
}),
'node_modules': Dir({
hassupport: Dir({
'package.json': File({
name: 'hassupport',
version: '7.7.7',
homepage: 'http://example.com/project',
support: 'http://example.com/project/donate'
})
})
})
}))

test('setup', function (t) {
fixture.remove(fixturepath)
fixture.create(fixturepath)
t.end()
})

test('support --json', function (t) {
common.npm(['support', '--json'], {cwd: fixturepath}, function (err, code, stdout, stderr) {
if (err) throw err
t.is(code, 0, 'exited 0')
t.is(stderr, '', 'no warnings')
var parsed
t.doesNotThrow(function () {
parsed = JSON.parse(stdout)
}, 'valid JSON')
t.deepEqual(
parsed,
[
{
name: 'hassupport',
version: '7.7.7',
homepage: 'http://example.com/project',
support: 'http://example.com/project/donate',
path: path.resolve(fixturepath, 'node_modules', 'hassupport')
}
],
'output data'
)
t.end()
})
})

test('support', function (t) {
common.npm(['support'], {cwd: fixturepath}, function (err, code, stdout, stderr) {
if (err) throw err
t.is(code, 0, 'exited 0')
t.is(stderr, '', 'no warnings')
t.includes(stdout, 'hassupport', 'outputs project name')
t.includes(stdout, '7.7.7', 'outputs project version')
t.includes(stdout, 'http://example.com/project', 'outputs contributor homepage')
t.includes(stdout, 'http://example.com/project/donate', 'outputs support link')
t.end()
})
})

test('cleanup', function (t) {
t.pass(fixturepath)
fixture.remove(fixturepath)
t.end()
})

0 comments on commit 266d076

Please sign in to comment.