From 370b36a36ca226840761e4214cbccaf2a1a90e3c Mon Sep 17 00:00:00 2001 From: Ruy Adorno Date: Wed, 12 May 2021 18:21:14 -0400 Subject: [PATCH] feat: add fund workspaces Add workspaces support to `npm fund` - Add lib/workspaces/arborist-cmd.js base class - Add ability to filter fund results to a specific set of workspaces - Added tests and docs Fixes: https://github.com/npm/statusboard/issues/301 PR-URL: https://github.com/npm/cli/pull/3241 Credit: @ruyadorno Close: #3241 Reviewed-by: @isaacs --- docs/content/commands/npm-fund.md | 56 +++++++++ lib/fund.js | 16 ++- lib/workspaces/arborist-cmd.js | 24 ++++ tap-snapshots/test/lib/fund.js.test.cjs | 20 ++++ .../test/lib/utils/npm-usage.js.test.cjs | 1 + test/lib/fund.js | 86 ++++++++++++++ test/lib/workspaces/arborist-cmd.js | 109 ++++++++++++++++++ 7 files changed, 307 insertions(+), 5 deletions(-) create mode 100644 lib/workspaces/arborist-cmd.js create mode 100644 test/lib/workspaces/arborist-cmd.js diff --git a/docs/content/commands/npm-fund.md b/docs/content/commands/npm-fund.md index aa1b26b9a8971..45c5dfaac2afc 100644 --- a/docs/content/commands/npm-fund.md +++ b/docs/content/commands/npm-fund.md @@ -8,6 +8,7 @@ description: Retrieve funding information ```bash npm fund [] +npm fund [-w ] ``` ### Description @@ -24,6 +25,43 @@ The list will avoid duplicated entries and will stack all packages that share the same url as a single entry. Thus, the list does not have the same shape of the output from `npm ls`. +#### Example + +### Workspaces support + +It's possible to filter the results to only include a single workspace and its +dependencies using the `workspace` config option. + +#### Example: + +Here's an example running `npm fund` in a project with a configured +workspace `a`: + +```bash +$ npm fund +test-workspaces-fund@1.0.0 ++-- https://example.com/a +| | `-- a@1.0.0 +| `-- https://example.com/maintainer +| `-- foo@1.0.0 ++-- https://example.com/npmcli-funding +| `-- @npmcli/test-funding +`-- https://example.com/org + `-- bar@2.0.0 +``` + +And here is an example of the expected result when filtering only by +a specific workspace `a` in the same project: + +```bash +$ npm fund -w a +test-workspaces-fund@1.0.0 +`-- https://example.com/a + | `-- a@1.0.0 + `-- https://example.com/maintainer + `-- foo@2.0.0 +``` + ### Configuration #### browser @@ -48,6 +86,23 @@ Show information in JSON format. Whether to represent the tree structure using unicode characters. Set it to `false` in order to use all-ansi output. +#### `workspace` + +* Default: +* Type: String (can be set multiple times) + +Enable running a command in the context of the configured workspaces of the +current project while filtering by running only the workspaces defined by +this configuration option. + +Valid values for the `workspace` config are either: +* Workspace names +* Path to a workspace directory +* Path to a parent workspace directory (will result to selecting all of the +nested workspaces) + +This value is not exported to the environment for child processes. + #### which * Type: Number @@ -61,3 +116,4 @@ If there are multiple funding sources, which 1-indexed source URL to open. * [npm docs](/commands/npm-docs) * [npm ls](/commands/npm-ls) * [npm config](/commands/npm-config) +* [npm workspaces](/using-npm/workspaces) diff --git a/lib/fund.js b/lib/fund.js index 25d3462f63869..55d2f65dc4b55 100644 --- a/lib/fund.js +++ b/lib/fund.js @@ -13,15 +13,14 @@ const { const completion = require('./utils/completion/installed-deep.js') const openUrl = require('./utils/open-url.js') +const ArboristWorkspaceCmd = require('./workspaces/arborist-cmd.js') const getPrintableName = ({ name, version }) => { const printableVersion = version ? `@${version}` : '' return `${name}${printableVersion}` } -const BaseCommand = require('./base-command.js') - -class Fund extends BaseCommand { +class Fund extends ArboristWorkspaceCmd { /* istanbul ignore next - see test/lib/load-all-commands.js */ static get description () { return 'Retrieve funding information' @@ -38,6 +37,7 @@ class Fund extends BaseCommand { 'json', 'browser', 'unicode', + 'workspace', 'which', ] } @@ -92,10 +92,16 @@ class Fund extends BaseCommand { return } + const fundingInfo = getFundingInfo(tree, { + ...this.flatOptions, + log: this.npm.log, + workspaces: this.workspaces, + }) + if (this.npm.config.get('json')) - this.npm.output(this.printJSON(getFundingInfo(tree))) + this.npm.output(this.printJSON(fundingInfo)) else - this.npm.output(this.printHuman(getFundingInfo(tree))) + this.npm.output(this.printHuman(fundingInfo)) } printJSON (fundingInfo) { diff --git a/lib/workspaces/arborist-cmd.js b/lib/workspaces/arborist-cmd.js new file mode 100644 index 0000000000000..f08843bd9ea5a --- /dev/null +++ b/lib/workspaces/arborist-cmd.js @@ -0,0 +1,24 @@ +// This is the base for all commands whose execWorkspaces just gets +// a list of workspace names and passes it on to new Arborist() to +// be able to run a filtered Arborist.reify() at some point. + +const BaseCommand = require('../base-command.js') +const getWorkspaces = require('../workspaces/get-workspaces.js') +class ArboristCmd extends BaseCommand { + /* istanbul ignore next - see test/lib/load-all-commands.js */ + static get params () { + return [ + 'workspace', + ] + } + + execWorkspaces (args, filters, cb) { + getWorkspaces(filters, { path: this.npm.localPrefix }) + .then(workspaces => { + this.workspaces = [...workspaces.keys()] + this.exec(args, cb) + }) + } +} + +module.exports = ArboristCmd diff --git a/tap-snapshots/test/lib/fund.js.test.cjs b/tap-snapshots/test/lib/fund.js.test.cjs index 7ad86ebeea7e9..c078beb7d9866 100644 --- a/tap-snapshots/test/lib/fund.js.test.cjs +++ b/tap-snapshots/test/lib/fund.js.test.cjs @@ -92,3 +92,23 @@ test-multiple-funding-sources@1.0.0 ` + +exports[`test/lib/fund.js TAP workspaces filter funding info by a specific workspace > should display only filtered workspace name and its deps 1`] = ` +workspaces-support@1.0.0 +\`-- https://example.com/a + | \`-- a@1.0.0 + \`-- http://example.com/c + \`-- c@1.0.0 + + +` + +exports[`test/lib/fund.js TAP workspaces filter funding info by a specific workspace > should display only filtered workspace path and its deps 1`] = ` +workspaces-support@1.0.0 +\`-- https://example.com/a + | \`-- a@1.0.0 + \`-- http://example.com/c + \`-- c@1.0.0 + + +` diff --git a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs index 9bcba775fd85b..84ffc44e33300 100644 --- a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs +++ b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs @@ -422,6 +422,7 @@ All commands: Options: [--json] [--browser|--browser ] [--unicode] + [-w|--workspace [-w|--workspace ...]] [--which ] Run "npm help fund" for more info diff --git a/test/lib/fund.js b/test/lib/fund.js index 41754d51f3589..65778fca50bd7 100644 --- a/test/lib/fund.js +++ b/test/lib/fund.js @@ -839,3 +839,89 @@ t.test('sub dep with fund info and a parent with no funding info', t => { t.end() }) }) + +t.test('workspaces', t => { + t.test('filter funding info by a specific workspace', async t => { + npm.localPrefix = npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'workspaces-support', + version: '1.0.0', + workspaces: ['packages/*'], + dependencies: { + d: '^1.0.0', + }, + }), + node_modules: { + a: t.fixture('symlink', '../packages/a'), + b: t.fixture('symlink', '../packages/b'), + c: { + 'package.json': JSON.stringify({ + name: 'c', + version: '1.0.0', + funding: [ + 'http://example.com/c', + 'http://example.com/c-other', + ], + }), + }, + d: { + 'package.json': JSON.stringify({ + name: 'd', + version: '1.0.0', + funding: 'http://example.com/d', + }), + }, + }, + packages: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + funding: 'https://example.com/a', + dependencies: { + c: '^1.0.0', + }, + }), + }, + b: { + 'package.json': JSON.stringify({ + name: 'b', + version: '1.0.0', + funding: 'http://example.com/b', + dependencies: { + d: '^1.0.0', + }, + }), + }, + }, + }) + + await new Promise((res, rej) => { + fund.execWorkspaces([], ['a'], (err) => { + if (err) + rej(err) + + t.matchSnapshot(result, + 'should display only filtered workspace name and its deps') + + result = '' + res() + }) + }) + + await new Promise((res, rej) => { + fund.execWorkspaces([], ['./packages/a'], (err) => { + if (err) + rej(err) + + t.matchSnapshot(result, + 'should display only filtered workspace path and its deps') + + result = '' + res() + }) + }) + }) + + t.end() +}) diff --git a/test/lib/workspaces/arborist-cmd.js b/test/lib/workspaces/arborist-cmd.js new file mode 100644 index 0000000000000..cceeb68dbd42a --- /dev/null +++ b/test/lib/workspaces/arborist-cmd.js @@ -0,0 +1,109 @@ +const { resolve } = require('path') +const t = require('tap') +const ArboristCmd = require('../../../lib/workspaces/arborist-cmd.js') + +t.test('arborist-cmd', async t => { + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'simple-workspaces-list', + version: '1.1.1', + workspaces: [ + 'a', + 'b', + 'group/*', + ], + }), + node_modules: { + abbrev: { + 'package.json': JSON.stringify({ name: 'abbrev', version: '1.1.1' }), + }, + a: t.fixture('symlink', '../a'), + b: t.fixture('symlink', '../b'), + }, + a: { + 'package.json': JSON.stringify({ name: 'a', version: '1.0.0' }), + }, + b: { + 'package.json': JSON.stringify({ name: 'b', version: '1.0.0' }), + }, + group: { + c: { + 'package.json': JSON.stringify({ + name: 'c', + version: '1.0.0', + dependencies: { + abbrev: '^1.1.1', + }, + }), + }, + d: { + 'package.json': JSON.stringify({ name: 'd', version: '1.0.0' }), + }, + }, + }) + + class TestCmd extends ArboristCmd {} + + const cmd = new TestCmd() + cmd.npm = { localPrefix: path } + + // check filtering for a single workspace name + cmd.exec = function (args, cb) { + t.same(this.workspaces, ['a'], 'should set array with single ws name') + t.same(args, ['foo'], 'should get received args') + cb() + } + await new Promise(res => { + cmd.execWorkspaces(['foo'], ['a'], res) + }) + + // check filtering single workspace by path + cmd.exec = function (args, cb) { + t.same(this.workspaces, ['a'], + 'should set array with single ws name from path') + cb() + } + await new Promise(res => { + cmd.execWorkspaces([], ['./a'], res) + }) + + // check filtering single workspace by full path + cmd.exec = function (args, cb) { + t.same(this.workspaces, ['a'], + 'should set array with single ws name from full path') + cb() + } + await new Promise(res => { + cmd.execWorkspaces([], [resolve(path, './a')], res) + }) + + // filtering multiple workspaces by name + cmd.exec = function (args, cb) { + t.same(this.workspaces, ['a', 'c'], + 'should set array with multiple listed ws names') + cb() + } + await new Promise(res => { + cmd.execWorkspaces([], ['a', 'c'], res) + }) + + // filtering multiple workspaces by path names + cmd.exec = function (args, cb) { + t.same(this.workspaces, ['a', 'c'], + 'should set array with multiple ws names from paths') + cb() + } + await new Promise(res => { + cmd.execWorkspaces([], ['./a', 'group/c'], res) + }) + + // filtering multiple workspaces by parent path name + cmd.exec = function (args, cb) { + t.same(this.workspaces, ['c', 'd'], + 'should set array with multiple ws names from a parent folder name') + cb() + } + await new Promise(res => { + cmd.execWorkspaces([], ['./group'], res) + }) +})