From 6722732cf8db830ec28f2b92d87155f61b1424c7 Mon Sep 17 00:00:00 2001 From: "Kyle E. Mitchell" Date: Thu, 11 Apr 2019 23:31:32 -0700 Subject: [PATCH 1/2] docs: add package.json "sustainability" property --- doc/files/package.json.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/doc/files/package.json.md b/doc/files/package.json.md index 95e77d34c5f9f..0272ba10e0bcd 100644 --- a/doc/files/package.json.md +++ b/doc/files/package.json.md @@ -170,6 +170,23 @@ Both email and url are optional either way. npm also sets a top-level "maintainers" field with your npm user info. +## sustainability + +You can specify an HTTP endpoint for up-to-date information about ways +to sustain development of your package so that people can learn more ways +to support your work: + + { "sustainability": "https://example.com/sustainability.json" } + +You may like to develop your sustainability file as a file in your +source repository: + + { "sustainability": "https://raw.githubusercontent.com/{user}/{repo}/master/sustainability.json" } + +The end point you specify should respond to unauthenticated GET +requests with a JSON body that conforms to the [sustainability data +schema](https://www.npmjs.com/package/sustainability-schema). + ## files The optional `files` field is an array of file patterns that describes From 4733aa01912bc4354d80eefb67a87ddb7f355146 Mon Sep 17 00:00:00 2001 From: "Kyle E. Mitchell" Date: Sun, 25 Aug 2019 00:25:21 -0700 Subject: [PATCH 2/2] install: report sustainability links --- lib/install.js | 57 ++++++++++++++++++++++--- lib/install/sustainability.js | 31 ++++++++++++++ package-lock.json | 53 ++++++++++++++++++++--- package.json | 2 + test/tap/install-sustainability.js | 68 ++++++++++++++++++++++++++++++ 5 files changed, 200 insertions(+), 11 deletions(-) create mode 100644 lib/install/sustainability.js create mode 100644 test/tap/install-sustainability.js diff --git a/lib/install.js b/lib/install.js index d2f705e1d1abd..85796af886d3b 100644 --- a/lib/install.js +++ b/lib/install.js @@ -110,6 +110,8 @@ var iferr = require('iferr') var validate = require('aproba') var uniq = require('lodash.uniq') var Bluebird = require('bluebird') +var getSustainability = require('get-sustainability') +var runParallel = require('run-parallel') // npm internal utils var npm = require('./npm.js') @@ -138,6 +140,7 @@ var validateArgs = require('./install/validate-args.js') var saveRequested = require('./install/save.js').saveRequested var saveShrinkwrap = require('./install/save.js').saveShrinkwrap var audit = require('./install/audit.js') +var sustainability = require('./install/sustainability.js') var getSaveType = require('./install/save.js').getSaveType var doSerialActions = require('./install/actions.js').doSerial var doReverseSerialActions = require('./install/actions.js').doReverseSerial @@ -780,9 +783,19 @@ Installer.prototype.printInstalled = function (cb) { }) } return Bluebird.try(() => { - if (!this.auditSubmission) return - return Bluebird.resolve(this.auditSubmission).timeout(10000).catch(() => null) - }).then((auditResult) => { + var tasks = {} + if (this.auditSubmission) { + tasks.audit = Bluebird.resolve(this.auditSubmission).timeout(10000).catch(() => null) + } + if (!npm.config.get('json') && !npm.config.get('parseable')) { + tasks.sustainability = Bluebird.fromCallback(function (cb) { + getSustainabilityData(diffs, cb) + }) + } + return Bluebird.props(tasks) + }).then((results) => { + var auditResult = results.audit + var sustainabilityResult = results.sustainability if (auditResult && !auditResult.metadata) { log.warn('audit', 'Audit result from registry missing metadata. This is probably an issue with the registry.') } @@ -792,12 +805,42 @@ Installer.prototype.printInstalled = function (cb) { } else if (npm.config.get('parseable')) { return this.printInstalledForParseable(diffs, auditResult) } else { - return this.printInstalledForHuman(diffs, auditResult) + return this.printInstalledForHuman(diffs, sustainabilityResult, auditResult) } }).asCallback(cb) } -Installer.prototype.printInstalledForHuman = function (diffs, auditResult) { +function getSustainabilityData (diffs, cb) { + var records = [] + diffs.forEach(function (action) { + var mutation = action[0] + var pkg = action[1] + if (pkg.failed) return + if (mutation === 'remove') return + var meta = pkg.package + var sustainability = meta.sustainability + if (sustainability && typeof sustainability === 'string') { + records.push({ + name: meta.name, + version: meta.version, + uri: sustainability + }) + } + }) + runParallel(records.map(function (record) { + return function (done) { + getSustainability({ uri: record.uri }, function (err, data) { + if (err) return done() // eat errors + record.data = data + done(null, record) + }) + } + }), function (_, results) { + cb(null, results.filter((e) => e)) + }) +} + +Installer.prototype.printInstalledForHuman = function (diffs, sustainabilityResult, auditResult) { var removed = 0 var added = 0 var updated = 0 @@ -872,7 +915,9 @@ Installer.prototype.printInstalledForHuman = function (diffs, auditResult) { report += ' in ' + ((Date.now() - this.started) / 1000) + 's' output(report) - return auditResult && audit.printInstallReport(auditResult) + if (auditResult) audit.printInstallReport(auditResult) + if (sustainabilityResult) sustainability.printInstallReport(sustainabilityResult) + else output('sustain') function packages (num) { return num + ' package' + (num > 1 ? 's' : '') diff --git a/lib/install/sustainability.js b/lib/install/sustainability.js new file mode 100644 index 0000000000000..266613ff2f881 --- /dev/null +++ b/lib/install/sustainability.js @@ -0,0 +1,31 @@ +'use strict' + +const output = require('../utils/output.js') + +exports.printInstallReport = function (result) { + if (result.length === 0) return + var report = 'Help sustain the packages you depend on!' + result.forEach(function (record) { + var name = record.name + var version = record.version + report += '\n' + name + '@' + version + ':\n' + report += record.data.contributors + .map(function (contributor) { + var line = ' ' + contributor.name + if (contributor.homepage) { + line += ' (' + contributor.homepage + ')' + } + line += '\n' + if (contributor.links) { + line += contributor.links + .map(function (link) { + return ' ' + link.uri + }) + .join('\n') + '\n' + } + return line + }) + .join('\n') + }) + output(report) +} diff --git a/package-lock.json b/package-lock.json index 56b143db5ae6e..e15b9ff929a91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2020,8 +2020,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "function-loop": { "version": "1.0.1", @@ -2118,6 +2117,18 @@ "pump": "^3.0.0" } }, + "get-sustainability": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-sustainability/-/get-sustainability-2.0.0.tgz", + "integrity": "sha512-ftZwfdFSrphUCYqkvvFdsT3eGScDjRlziyZIIUcbtXTrolznlA7uiOMNlgolAngJz4pbhY8TvsN9U0GXH3FDrw==", + "requires": { + "has": "^1.0.3", + "run-parallel-limit": "^1.0.5", + "sustainability-schema": "^2.0.0", + "tv4": "^1.3.0", + "url-parse": "^1.4.7" + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -2207,7 +2218,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -5189,6 +5199,11 @@ "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", "dev": true }, + "querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==" + }, "qw": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/qw/-/qw-1.0.1.tgz", @@ -5389,6 +5404,11 @@ } } }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, "resolve": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", @@ -5438,8 +5458,12 @@ "run-parallel": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", - "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", - "dev": true + "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==" + }, + "run-parallel-limit": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/run-parallel-limit/-/run-parallel-limit-1.0.5.tgz", + "integrity": "sha512-NsY+oDngvrvMxKB3G8ijBzIema6aYbQMD2bHOamvN52BysbIGTnEY2xsNyfrcr9GhY995/t/0nQN3R3oZvaDlg==" }, "run-queue": { "version": "1.0.3", @@ -5946,6 +5970,11 @@ "has-flag": "^3.0.0" } }, + "sustainability-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sustainability-schema/-/sustainability-schema-2.0.0.tgz", + "integrity": "sha512-LLCEan1jU1JXmbRXGn6KrN1jZkNQ8qruggN9Xz2Pxv2uAwOptanE4ijVL/yP8estwU7bqPvdjmKm8iDiwxhvNg==" + }, "table": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", @@ -6422,6 +6451,11 @@ "safe-buffer": "^5.0.1" } }, + "tv4": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", + "integrity": "sha1-0CDIRvrdUMhVq7JeuuzGj8EPeWM=" + }, "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -6543,6 +6577,15 @@ } } }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", diff --git a/package.json b/package.json index 24827e59cfd9a..1bca2157ce61c 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "fs-vacuum": "~1.2.10", "fs-write-stream-atomic": "~1.0.10", "gentle-fs": "^2.0.1", + "get-sustainability": "^2.0.0", "glob": "^7.1.3", "graceful-fs": "^4.1.15", "has-unicode": "~2.0.1", @@ -119,6 +120,7 @@ "request": "^2.88.0", "retry": "^0.12.0", "rimraf": "^2.6.3", + "run-parallel": "^1.1.9", "safe-buffer": "^5.1.2", "semver": "^5.6.0", "sha": "~2.0.1", diff --git a/test/tap/install-sustainability.js b/test/tap/install-sustainability.js new file mode 100644 index 0000000000000..fc648b67c2e2c --- /dev/null +++ b/test/tap/install-sustainability.js @@ -0,0 +1,68 @@ +'use strict' +var path = require('path') +var test = require('tap').test +var Tacks = require('tacks') +var Dir = Tacks.Dir +var File = Tacks.File +var common = require('../common-tap.js') + +var testdir = path.resolve(__dirname, path.basename(__filename, '.js')) +var fixture = new Tacks(Dir({ + node_modules: Dir({ + a: Dir({ + 'package.json': File({ + name: 'a', + version: '1.0.0', + dependencies: { + b: '1.0.0' + } + }), + node_modules: Dir({ + b: Dir({ + 'package.json': File({ + name: 'b', + version: '1.0.0' + }) + }) + }) + }) + }), + 'b-src': Dir({ + 'package.json': File({ + name: 'b', + version: '1.0.0', + sustainability: 'https://raw.githubusercontent.com/kemitchell/get-sustainability.js/master/sustainability.json' + }) + }) +})) + +function setup () { + cleanup() + fixture.create(testdir) +} + +function cleanup () { + fixture.remove(testdir) +} + +test('setup', function (t) { + setup() + t.end() +}) + +test('install', function (t) { + common.npm(['install', '--no-save', './b-src'], {cwd: testdir}, function (err, code, stdout, stderr) { + if (err) throw err + t.is(code, 0, 'installed successfully') + t.is(stderr, '', 'no warnings') + t.includes(stdout, 'sustain', 'says "sustain"') + t.includes(stdout, 'Kyle E. Mitchell', 'mentions contributor') + t.includes(stdout, 'kemitchell.com', 'mentions homepage') + t.end() + }) +}) + +test('cleanup', function (t) { + cleanup() + t.end() +})