Skip to content

Commit

Permalink
feat: add fund workspaces
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ruyadorno committed May 13, 2021
1 parent 076420c commit 370b36a
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 5 deletions.
56 changes: 56 additions & 0 deletions docs/content/commands/npm-fund.md
Expand Up @@ -8,6 +8,7 @@ description: Retrieve funding information

```bash
npm fund [<pkg>]
npm fund [-w <workspace-name>]
```

### Description
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
16 changes: 11 additions & 5 deletions lib/fund.js
Expand Up @@ -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'
Expand All @@ -38,6 +37,7 @@ class Fund extends BaseCommand {
'json',
'browser',
'unicode',
'workspace',
'which',
]
}
Expand Down Expand Up @@ -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) {
Expand Down
24 changes: 24 additions & 0 deletions 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
20 changes: 20 additions & 0 deletions tap-snapshots/test/lib/fund.js.test.cjs
Expand Up @@ -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
`
1 change: 1 addition & 0 deletions tap-snapshots/test/lib/utils/npm-usage.js.test.cjs
Expand Up @@ -422,6 +422,7 @@ All commands:
Options:
[--json] [--browser|--browser <browser>] [--unicode]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[--which <fundingSourceNumber>]
Run "npm help fund" for more info
Expand Down
86 changes: 86 additions & 0 deletions test/lib/fund.js
Expand Up @@ -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()
})
109 changes: 109 additions & 0 deletions 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)
})
})

0 comments on commit 370b36a

Please sign in to comment.