Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fund workspaces #3241

Merged
merged 1 commit into from May 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
})
})