diff options
author | Ruy Adorno <ruyadorno@hotmail.com> | 2021-05-14 23:37:47 +0300 |
---|---|---|
committer | isaacs <i@izs.me> | 2021-05-20 21:06:50 +0300 |
commit | c18626f047e3a0fedd3c86554a4a0a8f27925e77 (patch) | |
tree | 04bf1c9118e93ee350ebb959b801ecce08f9d413 | |
parent | b3add87e686968b7af3067c685d2561baf90e397 (diff) |
feat: add ls workspaces
- 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: https://github.com/npm/statusboard/issues/302
PR-URL: https://github.com/npm/cli/pull/3250
Credit: @ruyadorno
Close: #3250
Reviewed-by: @isaacs
-rw-r--r-- | docs/content/commands/npm-ls.md | 32 | ||||
-rw-r--r-- | lib/ls.js | 34 | ||||
-rw-r--r-- | tap-snapshots/test/lib/load-all-commands.js.test.cjs | 4 | ||||
-rw-r--r-- | tap-snapshots/test/lib/ls.js.test.cjs | 56 | ||||
-rw-r--r-- | tap-snapshots/test/lib/utils/npm-usage.js.test.cjs | 4 | ||||
-rw-r--r-- | test/lib/ls.js | 124 |
6 files changed, 234 insertions, 20 deletions
diff --git a/docs/content/commands/npm-ls.md b/docs/content/commands/npm-ls.md index 505dfd9c4..3c6621763 100644 --- a/docs/content/commands/npm-ls.md +++ b/docs/content/commands/npm-ls.md @@ -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 @@ -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' @@ -50,6 +50,7 @@ class LS extends BaseCommand { 'omit', 'link', 'unicode', + ...super.params, ] } @@ -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() @@ -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, @@ -129,7 +152,7 @@ class LS extends BaseCommand { .sort(sortAlphabetically) .map(augmentNodesWithMetadata({ args, - currentDepth: node[_depth], + currentDepth, nodeResult, seenNodes, })) @@ -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) { diff --git a/tap-snapshots/test/lib/load-all-commands.js.test.cjs b/tap-snapshots/test/lib/load-all-commands.js.test.cjs index c549c057c..f2d40d416 100644 --- a/tap-snapshots/test/lib/load-all-commands.js.test.cjs +++ b/tap-snapshots/test/lib/load-all-commands.js.test.cjs @@ -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 @@ -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 diff --git a/tap-snapshots/test/lib/ls.js.test.cjs b/tap-snapshots/test/lib/ls.js.test.cjs index 04f0be31f..f443d9cab 100644 --- a/tap-snapshots/test/lib/ls.js.test.cjs +++ b/tap-snapshots/test/lib/ls.js.test.cjs @@ -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`] = ` +[0mworkspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces[0m +[0m+-- [32ma@1.0.0[39m -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/a[0m +[0m| +-- c@1.0.0[0m +[0m| \`-- d@1.0.0 [90mdeduped[39m -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d[0m +[0m+-- [32mb@1.0.0[39m -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/b[0m +[0m+-- [32md@1.0.0[39m -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d[0m +[0m| \`-- foo@1.1.1[0m +[0m+-- [32me@1.0.0[39m -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/group/e[0m +[0m\`-- [32mf@1.0.0[39m -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/group/f[0m +[0m[0m +` + +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 ` 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 a67819eb0..a81a8f44b 100644 --- a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs +++ b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs @@ -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 @@ -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 diff --git a/test/lib/ls.js b/test/lib/ls.js index 276a06180..2cde31946 100644 --- a/test/lib/ls.js +++ b/test/lib/ls.js @@ -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: { @@ -1426,6 +1428,21 @@ 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({ @@ -1433,6 +1450,7 @@ t.test('ls', (t) => { version: '1.0.0', dependencies: { c: '^1.0.0', + d: '^1.0.0', }, }), }, @@ -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() }) }) }) |