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

Add support subcommand #246

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 10 additions & 0 deletions doc/files/package.json.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,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" }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth versioning this attribute so we can make changes to it in the future? It would allow us to add complexity over time as we see the need.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a string URL, it's a string URL. We don't get to version that standard. :-P

If it's not a string URL, it could be an object with a version property ... if/when we go there.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh. The latter is what I was thinking.

{
  "support": {
    "version": "1",
    "url": "https://example.com/project/support"
  }
}

But also it might be too weird to version individual attributes rather than the package.json schema as a whole.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does collide with what we are defining in https://github.com/nodejs/package-maintenance/blob/master/docs/drafts/PACKAGE-SUPPORT.md.

It would be good to avoid a conflict with that in that we'll be suggesting package maintainers add the larger structure.

Would what is suggested in maintenance/blob/master/docs/drafts/PACKAGE-SUPPORT.md be possible if qualified through a later version? And if so how would we have to update what is in there if we wanted to suggest the larger structure start to be used today? Alternatively, we could change "support" to something else but then it would potentially duplicate info.


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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
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()
})