Skip to content

Commit 370b36a

Browse files
committed
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: npm/statusboard#301 PR-URL: #3241 Credit: @ruyadorno Close: #3241 Reviewed-by: @isaacs
1 parent 076420c commit 370b36a

File tree

7 files changed

+307
-5
lines changed

7 files changed

+307
-5
lines changed

docs/content/commands/npm-fund.md

+56
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ description: Retrieve funding information
88

99
```bash
1010
npm fund [<pkg>]
11+
npm fund [-w <workspace-name>]
1112
```
1213

1314
### Description
@@ -24,6 +25,43 @@ The list will avoid duplicated entries and will stack all packages that
2425
share the same url as a single entry. Thus, the list does not have the same
2526
shape of the output from `npm ls`.
2627

28+
#### Example
29+
30+
### Workspaces support
31+
32+
It's possible to filter the results to only include a single workspace and its
33+
dependencies using the `workspace` config option.
34+
35+
#### Example:
36+
37+
Here's an example running `npm fund` in a project with a configured
38+
workspace `a`:
39+
40+
```bash
41+
$ npm fund
42+
43+
+-- https://example.com/a
44+
45+
| `-- https://example.com/maintainer
46+
47+
+-- https://example.com/npmcli-funding
48+
| `-- @npmcli/test-funding
49+
`-- https://example.com/org
50+
51+
```
52+
53+
And here is an example of the expected result when filtering only by
54+
a specific workspace `a` in the same project:
55+
56+
```bash
57+
$ npm fund -w a
58+
59+
`-- https://example.com/a
60+
61+
`-- https://example.com/maintainer
62+
63+
```
64+
2765
### Configuration
2866

2967
#### browser
@@ -48,6 +86,23 @@ Show information in JSON format.
4886
Whether to represent the tree structure using unicode characters.
4987
Set it to `false` in order to use all-ansi output.
5088

89+
#### `workspace`
90+
91+
* Default:
92+
* Type: String (can be set multiple times)
93+
94+
Enable running a command in the context of the configured workspaces of the
95+
current project while filtering by running only the workspaces defined by
96+
this configuration option.
97+
98+
Valid values for the `workspace` config are either:
99+
* Workspace names
100+
* Path to a workspace directory
101+
* Path to a parent workspace directory (will result to selecting all of the
102+
nested workspaces)
103+
104+
This value is not exported to the environment for child processes.
105+
51106
#### which
52107

53108
* Type: Number
@@ -61,3 +116,4 @@ If there are multiple funding sources, which 1-indexed source URL to open.
61116
* [npm docs](/commands/npm-docs)
62117
* [npm ls](/commands/npm-ls)
63118
* [npm config](/commands/npm-config)
119+
* [npm workspaces](/using-npm/workspaces)

lib/fund.js

+11-5
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,14 @@ const {
1313

1414
const completion = require('./utils/completion/installed-deep.js')
1515
const openUrl = require('./utils/open-url.js')
16+
const ArboristWorkspaceCmd = require('./workspaces/arborist-cmd.js')
1617

1718
const getPrintableName = ({ name, version }) => {
1819
const printableVersion = version ? `@${version}` : ''
1920
return `${name}${printableVersion}`
2021
}
2122

22-
const BaseCommand = require('./base-command.js')
23-
24-
class Fund extends BaseCommand {
23+
class Fund extends ArboristWorkspaceCmd {
2524
/* istanbul ignore next - see test/lib/load-all-commands.js */
2625
static get description () {
2726
return 'Retrieve funding information'
@@ -38,6 +37,7 @@ class Fund extends BaseCommand {
3837
'json',
3938
'browser',
4039
'unicode',
40+
'workspace',
4141
'which',
4242
]
4343
}
@@ -92,10 +92,16 @@ class Fund extends BaseCommand {
9292
return
9393
}
9494

95+
const fundingInfo = getFundingInfo(tree, {
96+
...this.flatOptions,
97+
log: this.npm.log,
98+
workspaces: this.workspaces,
99+
})
100+
95101
if (this.npm.config.get('json'))
96-
this.npm.output(this.printJSON(getFundingInfo(tree)))
102+
this.npm.output(this.printJSON(fundingInfo))
97103
else
98-
this.npm.output(this.printHuman(getFundingInfo(tree)))
104+
this.npm.output(this.printHuman(fundingInfo))
99105
}
100106

101107
printJSON (fundingInfo) {

lib/workspaces/arborist-cmd.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// This is the base for all commands whose execWorkspaces just gets
2+
// a list of workspace names and passes it on to new Arborist() to
3+
// be able to run a filtered Arborist.reify() at some point.
4+
5+
const BaseCommand = require('../base-command.js')
6+
const getWorkspaces = require('../workspaces/get-workspaces.js')
7+
class ArboristCmd extends BaseCommand {
8+
/* istanbul ignore next - see test/lib/load-all-commands.js */
9+
static get params () {
10+
return [
11+
'workspace',
12+
]
13+
}
14+
15+
execWorkspaces (args, filters, cb) {
16+
getWorkspaces(filters, { path: this.npm.localPrefix })
17+
.then(workspaces => {
18+
this.workspaces = [...workspaces.keys()]
19+
this.exec(args, cb)
20+
})
21+
}
22+
}
23+
24+
module.exports = ArboristCmd

tap-snapshots/test/lib/fund.js.test.cjs

+20
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,23 @@ [email protected]
9292
9393
9494
`
95+
96+
exports[`test/lib/fund.js TAP workspaces filter funding info by a specific workspace > should display only filtered workspace name and its deps 1`] = `
97+
98+
\`-- https://example.com/a
99+
100+
\`-- http://example.com/c
101+
102+
103+
104+
`
105+
106+
exports[`test/lib/fund.js TAP workspaces filter funding info by a specific workspace > should display only filtered workspace path and its deps 1`] = `
107+
108+
\`-- https://example.com/a
109+
110+
\`-- http://example.com/c
111+
112+
113+
114+
`

tap-snapshots/test/lib/utils/npm-usage.js.test.cjs

+1
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ All commands:
422422
423423
Options:
424424
[--json] [--browser|--browser <browser>] [--unicode]
425+
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
425426
[--which <fundingSourceNumber>]
426427
427428
Run "npm help fund" for more info

test/lib/fund.js

+86
Original file line numberDiff line numberDiff line change
@@ -839,3 +839,89 @@ t.test('sub dep with fund info and a parent with no funding info', t => {
839839
t.end()
840840
})
841841
})
842+
843+
t.test('workspaces', t => {
844+
t.test('filter funding info by a specific workspace', async t => {
845+
npm.localPrefix = npm.prefix = t.testdir({
846+
'package.json': JSON.stringify({
847+
name: 'workspaces-support',
848+
version: '1.0.0',
849+
workspaces: ['packages/*'],
850+
dependencies: {
851+
d: '^1.0.0',
852+
},
853+
}),
854+
node_modules: {
855+
a: t.fixture('symlink', '../packages/a'),
856+
b: t.fixture('symlink', '../packages/b'),
857+
c: {
858+
'package.json': JSON.stringify({
859+
name: 'c',
860+
version: '1.0.0',
861+
funding: [
862+
'http://example.com/c',
863+
'http://example.com/c-other',
864+
],
865+
}),
866+
},
867+
d: {
868+
'package.json': JSON.stringify({
869+
name: 'd',
870+
version: '1.0.0',
871+
funding: 'http://example.com/d',
872+
}),
873+
},
874+
},
875+
packages: {
876+
a: {
877+
'package.json': JSON.stringify({
878+
name: 'a',
879+
version: '1.0.0',
880+
funding: 'https://example.com/a',
881+
dependencies: {
882+
c: '^1.0.0',
883+
},
884+
}),
885+
},
886+
b: {
887+
'package.json': JSON.stringify({
888+
name: 'b',
889+
version: '1.0.0',
890+
funding: 'http://example.com/b',
891+
dependencies: {
892+
d: '^1.0.0',
893+
},
894+
}),
895+
},
896+
},
897+
})
898+
899+
await new Promise((res, rej) => {
900+
fund.execWorkspaces([], ['a'], (err) => {
901+
if (err)
902+
rej(err)
903+
904+
t.matchSnapshot(result,
905+
'should display only filtered workspace name and its deps')
906+
907+
result = ''
908+
res()
909+
})
910+
})
911+
912+
await new Promise((res, rej) => {
913+
fund.execWorkspaces([], ['./packages/a'], (err) => {
914+
if (err)
915+
rej(err)
916+
917+
t.matchSnapshot(result,
918+
'should display only filtered workspace path and its deps')
919+
920+
result = ''
921+
res()
922+
})
923+
})
924+
})
925+
926+
t.end()
927+
})

test/lib/workspaces/arborist-cmd.js

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
const { resolve } = require('path')
2+
const t = require('tap')
3+
const ArboristCmd = require('../../../lib/workspaces/arborist-cmd.js')
4+
5+
t.test('arborist-cmd', async t => {
6+
const path = t.testdir({
7+
'package.json': JSON.stringify({
8+
name: 'simple-workspaces-list',
9+
version: '1.1.1',
10+
workspaces: [
11+
'a',
12+
'b',
13+
'group/*',
14+
],
15+
}),
16+
node_modules: {
17+
abbrev: {
18+
'package.json': JSON.stringify({ name: 'abbrev', version: '1.1.1' }),
19+
},
20+
a: t.fixture('symlink', '../a'),
21+
b: t.fixture('symlink', '../b'),
22+
},
23+
a: {
24+
'package.json': JSON.stringify({ name: 'a', version: '1.0.0' }),
25+
},
26+
b: {
27+
'package.json': JSON.stringify({ name: 'b', version: '1.0.0' }),
28+
},
29+
group: {
30+
c: {
31+
'package.json': JSON.stringify({
32+
name: 'c',
33+
version: '1.0.0',
34+
dependencies: {
35+
abbrev: '^1.1.1',
36+
},
37+
}),
38+
},
39+
d: {
40+
'package.json': JSON.stringify({ name: 'd', version: '1.0.0' }),
41+
},
42+
},
43+
})
44+
45+
class TestCmd extends ArboristCmd {}
46+
47+
const cmd = new TestCmd()
48+
cmd.npm = { localPrefix: path }
49+
50+
// check filtering for a single workspace name
51+
cmd.exec = function (args, cb) {
52+
t.same(this.workspaces, ['a'], 'should set array with single ws name')
53+
t.same(args, ['foo'], 'should get received args')
54+
cb()
55+
}
56+
await new Promise(res => {
57+
cmd.execWorkspaces(['foo'], ['a'], res)
58+
})
59+
60+
// check filtering single workspace by path
61+
cmd.exec = function (args, cb) {
62+
t.same(this.workspaces, ['a'],
63+
'should set array with single ws name from path')
64+
cb()
65+
}
66+
await new Promise(res => {
67+
cmd.execWorkspaces([], ['./a'], res)
68+
})
69+
70+
// check filtering single workspace by full path
71+
cmd.exec = function (args, cb) {
72+
t.same(this.workspaces, ['a'],
73+
'should set array with single ws name from full path')
74+
cb()
75+
}
76+
await new Promise(res => {
77+
cmd.execWorkspaces([], [resolve(path, './a')], res)
78+
})
79+
80+
// filtering multiple workspaces by name
81+
cmd.exec = function (args, cb) {
82+
t.same(this.workspaces, ['a', 'c'],
83+
'should set array with multiple listed ws names')
84+
cb()
85+
}
86+
await new Promise(res => {
87+
cmd.execWorkspaces([], ['a', 'c'], res)
88+
})
89+
90+
// filtering multiple workspaces by path names
91+
cmd.exec = function (args, cb) {
92+
t.same(this.workspaces, ['a', 'c'],
93+
'should set array with multiple ws names from paths')
94+
cb()
95+
}
96+
await new Promise(res => {
97+
cmd.execWorkspaces([], ['./a', 'group/c'], res)
98+
})
99+
100+
// filtering multiple workspaces by parent path name
101+
cmd.exec = function (args, cb) {
102+
t.same(this.workspaces, ['c', 'd'],
103+
'should set array with multiple ws names from a parent folder name')
104+
cb()
105+
}
106+
await new Promise(res => {
107+
cmd.execWorkspaces([], ['./group'], res)
108+
})
109+
})

0 commit comments

Comments
 (0)