Skip to content

Commit

Permalink
support: simplify to just collecting and showing URLs
Browse files Browse the repository at this point in the history
  • Loading branch information
kemitchell committed Sep 10, 2019
1 parent b892bf8 commit 1901d25
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 370 deletions.
19 changes: 5 additions & 14 deletions doc/files/package.json.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,22 +172,13 @@ npm also sets a top-level "maintainers" field with your npm user info.

## support

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

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

For example, you might like to develop your support data file in your
source code repository:

{ "support": "https://raw.githubusercontent.com/{user}/{repo}/master/support.json" }

The URL you specify should respond to unauthenticated GET requests
with a JSON object. If the JSON object contains a `contributors`
array, `npm support` will interpret it as a `support.json` file.
If the JSON object contains a `versions` array, `npm support`
will interpret it as [Node.js Package Maintenance Working
Group](https://github.com/nodejs/package-maintenance) metadata.
Users can use the `npm support` subcommand to all the dependencies of
their project with `support` URLs.

## files

Expand Down
57 changes: 0 additions & 57 deletions doc/files/support.json

This file was deleted.

5 changes: 4 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 @@ -811,7 +812,9 @@ Installer.prototype.printInstalledForHuman = function (diffs, auditResult) {
var mutation = action[0]
var pkg = action[1]
if (pkg.failed) return
if (mutation !== 'remove' && pkg.package.support) {
if (
mutation !== 'remove' && validSupportURL(pkg.package.support)
) {
haveSupportable = true
}
if (mutation === 'remove') {
Expand Down
232 changes: 44 additions & 188 deletions lib/support.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
'use strict'

var npm = require('./npm.js')
var output = require('./utils/output.js')
var readPackageTree = require('read-package-tree')
var runParallelLimit = require('run-parallel-limit')
var simpleGet = require('simple-get')
var semver = require('semver')
var hasANSI = require('has-ansi')
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

Expand All @@ -18,7 +17,6 @@ support.usage = usage(

support.completion = function (opts, cb) {
const argv = opts.conf.argv.remain

switch (argv[2]) {
case 'support':
return cb(null, [])
Expand All @@ -27,206 +25,64 @@ support.completion = function (opts, cb) {
}
}

// Compare lib/ls.js.
function support (args, silent, cb) {
readPackageTree(npm.dir, function (error, tree) {
if (error) return cb(error)
var supportablePackages = Array.from(findSupportablePackages(tree))
downloadSupportData(supportablePackages, function (error, data) {
if (error) return cb(error)

if (typeof cb !== 'function') {
cb = silent
silent = false
}
if (silent) return cb(null, data)

var out
var json = npm.config.get('json')
if (json) {
out = JSON.stringify(data, null, 2)
} else {
out = data
.sort(function (a, b) {
var comparison = a.name.localeCompare(b.name)
return comparison === 0
? semver.compare(a.version, b.version)
: comparison
})
.map(displaySupportData)
.join('\n\n')
}
output(out)
if (error) process.exitCode = 1
cb(error, data)
})
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 findSupportablePackages (root) {
var set = new Set()
function findPackages (root) {
const set = new Set()
iterate(root)
return set
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) {
var metadata = node.package
if (metadata.support) {
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,
parent: node.parent,
path: node.path
support: metadata.support
})
}
if (node.children) iterate(node)
}
}

function downloadSupportData (supportablePackages, cb) {
var cache = new Map()
var headers = { 'user-agent': npm.config.get('user-agent') }
runParallelLimit(supportablePackages.map(function (entry) {
return function task (done) {
var url = entry.support
get(url, function (error, response, projectData) {
if (error) {
return done(null, {
url: url,
error: 'could not download data'
})
}
if (typeof projectData !== 'object' || Array.isArray(projectData)) {
return done(null, {
url: url,
error: 'not an object'
})
}
var contributors = projectData.contributors
if (!Array.isArray(contributors)) {
return done(null, projectData)
}
runParallelLimit(contributors.map(function (contributor) {
return function (done) {
if (
typeof contributor !== 'object' ||
typeof contributor.url !== 'string'
) {
return setImmediate(function () {
done(null, contributor)
})
}
get(contributor.url, function (error, response, contributorData) {
if (error) {
return done(null, {
url: contributor.url,
error: error
})
}
contributorData.url = contributor.url
var result = {
name: contributorData.name,
type: contributorData.type,
url: contributor.url
}
if (looksLikeURL(contributorData.homepage)) {
result.homepage = contributorData.homepage
}
if (
Array.isArray(contributorData.links) &&
contributorData.links.every(function (element) {
return looksLikeURL(element)
})
) {
result.links = contributorData.links
}
done(null, result)
})
}
}), 5, function (error, resolvedContributors) {
if (error) return done(error)
done(null, {
name: entry.name,
version: entry.version,
url: entry.support,
homepage: entry.homepage,
contributors: resolvedContributors
})
})
})
}
}), 5, cb)

function get (url, cb) {
var cached = cache.get(url)
if (cached) {
return setImmediate(function () {
cb(null, { cached: true }, cached)
})
}
simpleGet.concat({
url: url,
json: true,
headers: headers
}, function (err, response, data) {
if (err) return cb(err)
cache.set(url, data)
cb(null, response, data)
})
}
}

function displaySupportData (entry) {
var returned = [entry.name + '@' + entry.version]
if (looksLikeURL(entry.homepage)) {
returned[0] += ' (' + entry.homepage + ')'
}
if (Array.isArray(entry.contributors)) {
entry.contributors.forEach(function (contributor) {
var name = contributor.name
if (looksLikeSafeString(name)) {
var item = ['- ' + name]
var email = contributor.email
if (looksLikeSafeString(email)) {
item[0] += ' <' + email + '>'
}
var homepage = contributor.homepage
if (looksLikeURL(homepage)) {
item[0] += ' (' + homepage + ')'
}
var links = contributor.links
if (Array.isArray(links)) {
links.forEach(function (link) {
if (looksLikeURL(link)) item.push(' ' + link)
})
}
returned.push(item.join('\n'))
}
})
}
return returned.join('\n')
}

function looksLikeSafeString (argument) {
return (
typeof argument === 'string' &&
argument.length > 0 &&
argument.length < 80 &&
!hasANSI(argument)
)
}

function looksLikeURL (argument) {
return (
looksLikeSafeString(argument) &&
(
argument.indexOf('https://') === 0 ||
argument.indexOf('http://') === 0
)
)
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
}
Loading

0 comments on commit 1901d25

Please sign in to comment.