Skip to content

Commit

Permalink
feat: add ls workspaces
Browse files Browse the repository at this point in the history
- Add listing workspaces deps by default in `npm ls`
- Add ability to filter the result tree by workspace using the -w config
- Added tests and docs

Fixes: npm/statusboard#302

PR-URL: #3250
Credit: @ruyadorno
Close: #3250
Reviewed-by: @isaacs
  • Loading branch information
ruyadorno authored and isaacs committed May 20, 2021
1 parent b3add87 commit c18626f
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 20 deletions.
32 changes: 32 additions & 0 deletions docs/content/commands/npm-ls.md
Expand Up @@ -177,6 +177,38 @@ When used with `npm ls`, only show packages that are linked.
When set to true, npm uses unicode characters in the tree output. When
false, it uses ascii characters instead of unicode glyphs.

#### `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)

When set for the `npm init` command, this may be set to the folder of a
workspace which does not yet exist, to create the folder and set it up as a
brand new workspace within the project.

This value is not exported to the environment for child processes.

#### `workspaces`

* Default: false
* Type: Boolean

Enable running a command in the context of **all** the configured
workspaces.

This value is not exported to the environment for child processes.

<!-- AUTOGENERATED CONFIG DESCRIPTIONS END -->

### See Also
Expand Down
34 changes: 29 additions & 5 deletions lib/ls.js
Expand Up @@ -20,9 +20,9 @@ const _parent = Symbol('parent')
const _problems = Symbol('problems')
const _required = Symbol('required')
const _type = Symbol('type')
const BaseCommand = require('./base-command.js')
const ArboristWorkspaceCmd = require('./workspaces/arborist-cmd.js')

class LS extends BaseCommand {
class LS extends ArboristWorkspaceCmd {
/* istanbul ignore next - see test/lib/load-all-commands.js */
static get description () {
return 'List installed packages'
Expand Down Expand Up @@ -50,6 +50,7 @@ class LS extends BaseCommand {
'omit',
'link',
'unicode',
...super.params,
]
}

Expand Down Expand Up @@ -88,6 +89,25 @@ class LS extends BaseCommand {
})
const tree = await this.initTree({arb, args })

// filters by workspaces nodes when using -w <workspace-name>
// We only have to filter the first layer of edges, so we don't
// explore anything that isn't part of the selected workspace set.
let wsNodes
if (this.workspaces && this.workspaces.length)
wsNodes = arb.workspaceNodes(tree, this.workspaces)
const filterBySelectedWorkspaces = edge => {
if (!wsNodes || !wsNodes.length)
return true

if (edge.from.isProjectRoot) {
return edge.to &&
edge.to.isWorkspace &
wsNodes.includes(edge.to.target)
}

return true
}

const seenItems = new Set()
const seenNodes = new Map()
const problems = new Set()
Expand All @@ -109,11 +129,14 @@ class LS extends BaseCommand {
// `nodeResult` is going to be the returned `item` from `visit`
getChildren (node, nodeResult) {
const seenPaths = new Set()
const workspace = node.isWorkspace
const currentDepth = workspace ? 0 : node[_depth]
const shouldSkipChildren =
!(node instanceof Arborist.Node) || (node[_depth] > depthToPrint)
!(node instanceof Arborist.Node) || (currentDepth > depthToPrint)
return (shouldSkipChildren)
? []
: [...(node.target || node).edgesOut.values()]
.filter(filterBySelectedWorkspaces)
.filter(filterByEdgesTypes({
dev,
development,
Expand All @@ -129,7 +152,7 @@ class LS extends BaseCommand {
.sort(sortAlphabetically)
.map(augmentNodesWithMetadata({
args,
currentDepth: node[_depth],
currentDepth,
nodeResult,
seenNodes,
}))
Expand Down Expand Up @@ -257,7 +280,8 @@ const augmentItemWithIncludeMetadata = (node, item) => {

const getHumanOutputItem = (node, { args, color, global, long }) => {
const { pkgid, path } = node
let printable = pkgid
const workspacePkgId = color ? chalk.green(pkgid) : pkgid
let printable = node.isWorkspace ? workspacePkgId : pkgid

// special formatting for top-level package name
if (node.isRoot) {
Expand Down
4 changes: 4 additions & 0 deletions tap-snapshots/test/lib/load-all-commands.js.test.cjs
Expand Up @@ -539,6 +539,8 @@ Options:
[-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth <depth>]
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]] [--link]
[--unicode]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces]
alias: la
Expand Down Expand Up @@ -587,6 +589,8 @@ Options:
[-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth <depth>]
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]] [--link]
[--unicode]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces]
alias: list
Expand Down
56 changes: 51 additions & 5 deletions tap-snapshots/test/lib/ls.js.test.cjs
Expand Up @@ -478,17 +478,63 @@ exports[`test/lib/ls.js TAP ls json read problems > should print empty result 1`
`

exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should filter by parent folder workspace config 1`] = `
workspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
+-- e@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/group/e
\`-- f@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/group/f
`

exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should filter single workspace 1`] = `
filter-by-child-of-missing-dep@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
workspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
+-- a@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/a
| \`-- d@1.0.0 deduped -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
\`-- d@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
`

exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should filter using workspace config 1`] = `
workspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
\`-- a@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/a
+-- c@1.0.0
\`-- d@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
\`-- foo@1.1.1
\`-- bar@1.0.0
`

exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should list workspaces properly 1`] = `
filter-by-child-of-missing-dep@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should list --all workspaces properly 1`] = `
workspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
+-- a@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/a
| \`-- c@1.0.0
\`-- b@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/b
| +-- c@1.0.0
| \`-- d@1.0.0 deduped -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
+-- b@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/b
+-- d@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
| \`-- foo@1.1.1
| \`-- bar@1.0.0
+-- e@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/group/e
\`-- f@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/group/f
`

exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should list workspaces properly with default configs 1`] = `
workspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
+-- a@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/a
| +-- c@1.0.0
| \`-- d@1.0.0 deduped -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
+-- b@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/b
+-- d@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
| \`-- foo@1.1.1
+-- e@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/group/e
\`-- f@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/group/f

`

exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should print all tree and filter by dep within only the ws subtree 1`] = `
workspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
\`-- d@1.0.0 -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
\`-- foo@1.1.1
\`-- bar@1.0.0
`

Expand Down
4 changes: 4 additions & 0 deletions tap-snapshots/test/lib/utils/npm-usage.js.test.cjs
Expand Up @@ -640,6 +640,8 @@ All commands:
[-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth <depth>]
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]] [--link]
[--unicode]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces]
alias: la
Expand Down Expand Up @@ -682,6 +684,8 @@ All commands:
[-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth <depth>]
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]] [--link]
[--unicode]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces]
alias: list
Expand Down
124 changes: 114 additions & 10 deletions test/lib/ls.js
Expand Up @@ -1407,14 +1407,16 @@ t.test('ls', (t) => {
})
})

t.test('loading a tree containing workspaces', (t) => {
npm.prefix = t.testdir({
t.test('loading a tree containing workspaces', async (t) => {
npm.localPrefix = npm.prefix = t.testdir({
'package.json': JSON.stringify({
name: 'filter-by-child-of-missing-dep',
name: 'workspaces-tree',
version: '1.0.0',
workspaces: [
'./a',
'./b',
'./d',
'./group/*',
],
}),
node_modules: {
Expand All @@ -1426,13 +1428,29 @@ t.test('ls', (t) => {
version: '1.0.0',
}),
},
d: t.fixture('symlink', '../d'),
e: t.fixture('symlink', '../group/e'),
f: t.fixture('symlink', '../group/f'),
foo: {
'package.json': JSON.stringify({
name: 'foo',
version: '1.1.1',
dependencies: {
bar: '^1.0.0',
},
}),
},
bar: {
'package.json': JSON.stringify({ name: 'bar', version: '1.0.0' }),
},
},
a: {
'package.json': JSON.stringify({
name: 'a',
version: '1.0.0',
dependencies: {
c: '^1.0.0',
d: '^1.0.0',
},
}),
},
Expand All @@ -1442,18 +1460,104 @@ t.test('ls', (t) => {
version: '1.0.0',
}),
},
d: {
'package.json': JSON.stringify({
name: 'd',
version: '1.0.0',
dependencies: {
foo: '^1.1.1',
},
}),
},
group: {
e: {
'package.json': JSON.stringify({
name: 'e',
version: '1.0.0',
}),
},
f: {
'package.json': JSON.stringify({
name: 'f',
version: '1.0.0',
}),
},
},
})

ls.exec([], (err) => {
t.error(err, 'should NOT have ELSPROBLEMS error code')
t.matchSnapshot(redactCwd(result), 'should list workspaces properly')
await new Promise((res, rej) => {
config.all = false
config.depth = 0
npm.color = true
ls.exec([], (err) => {
if (err)
rej(err)

t.matchSnapshot(redactCwd(result),
'should list workspaces properly with default configs')
config.all = true
config.depth = Infinity
npm.color = false
res()
})
})

// --all
await new Promise((res, rej) => {
ls.exec([], (err) => {
if (err)
rej(err)

t.matchSnapshot(redactCwd(result),
'should list --all workspaces properly')
res()
})
})

// filter out a single workspace using args
await new Promise((res, rej) => {
ls.exec(['d'], (err) => {
if (err)
rej(err)

// should also be able to filter out one of the workspaces
ls.exec(['a'], (err) => {
t.error(err, 'should NOT have ELSPROBLEMS error code when filter')
t.matchSnapshot(redactCwd(result), 'should filter single workspace')
res()
})
})

// filter out a single workspace and its deps using workspaces filters
await new Promise((res, rej) => {
ls.execWorkspaces([], ['a'], (err) => {
if (err)
rej(err)

t.matchSnapshot(redactCwd(result),
'should filter using workspace config')
res()
})
})

// filter out a workspace by parent path
await new Promise((res, rej) => {
ls.execWorkspaces([], ['./group'], (err) => {
if (err)
rej(err)

t.matchSnapshot(redactCwd(result),
'should filter by parent folder workspace config')
res()
})
})

// filter by a dep within a workspaces sub tree
await new Promise((res, rej) => {
ls.execWorkspaces(['bar'], ['d'], (err) => {
if (err)
rej(err)

t.end()
t.matchSnapshot(redactCwd(result),
'should print all tree and filter by dep within only the ws subtree')
res()
})
})
})
Expand Down

0 comments on commit c18626f

Please sign in to comment.