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 numberDiff line numberDiff 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
Copy Markdown

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
Copy Markdown
ContributorAuthor

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
Copy Markdown

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
Copy Markdown

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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()
})
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
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 numberDiff line numberDiff 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
Copy Markdown

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
Copy Markdown
ContributorAuthor

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
Copy Markdown

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
Copy Markdown

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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()
})
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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
Copy Markdown

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
Copy Markdown
ContributorAuthor

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
Copy Markdown

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
Copy Markdown

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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()
})
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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
Copy Markdown

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
Copy Markdown
ContributorAuthor

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
Copy Markdown

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
Copy Markdown

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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()
})
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
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 numberDiff line numberDiff 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
Copy Markdown

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
Copy Markdown
ContributorAuthor

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
Copy Markdown

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
Copy Markdown

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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()
})
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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
Copy Markdown

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
Copy Markdown
ContributorAuthor

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
Copy Markdown

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
Copy Markdown

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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()
})
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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 numberDiff line numberDiff 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
Copy Markdown

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
Copy Markdown
ContributorAuthor

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
Copy Markdown

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
Copy Markdown

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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()
})
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
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 numberDiff line numberDiff 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
Copy Markdown

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
Copy Markdown
ContributorAuthor

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
Copy Markdown

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
Copy Markdown

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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()
})