Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 6.13.0 #276

Merged
merged 19 commits into from
Nov 5, 2019
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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
kemitchell authored and ruyadorno committed Nov 5, 2019

Verified

This commit was signed with the committer’s verified signature.
ruyadorno Ruy Adorno
commit 266d07681f99e212e06d52b00aa41ad7b7c54467
10 changes: 10 additions & 0 deletions docs/content/configuring-npm/package-json.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions lib/config/cmd-list.js
Original file line number Diff line number Diff line change
@@ -91,6 +91,7 @@ var cmdList = [
'token',
'profile',
'audit',
'support',
'org',

'help',
15 changes: 14 additions & 1 deletion lib/install.js
Original file line number Diff line number Diff line change
@@ -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')
@@ -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') {
@@ -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' : '')
88 changes: 88 additions & 0 deletions lib/support.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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()
})