Skip to content

Commit c18626f

Browse files
ruyadornoisaacs
authored andcommitted
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: npm/statusboard#302 PR-URL: #3250 Credit: @ruyadorno Close: #3250 Reviewed-by: @isaacs
1 parent b3add87 commit c18626f

File tree

6 files changed

+234
-20
lines changed

6 files changed

+234
-20
lines changed

docs/content/commands/npm-ls.md

+32
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,38 @@ When used with `npm ls`, only show packages that are linked.
177177
When set to true, npm uses unicode characters in the tree output. When
178178
false, it uses ascii characters instead of unicode glyphs.
179179

180+
#### `workspace`
181+
182+
* Default:
183+
* Type: String (can be set multiple times)
184+
185+
Enable running a command in the context of the configured workspaces of the
186+
current project while filtering by running only the workspaces defined by
187+
this configuration option.
188+
189+
Valid values for the `workspace` config are either:
190+
191+
* Workspace names
192+
* Path to a workspace directory
193+
* Path to a parent workspace directory (will result to selecting all of the
194+
nested workspaces)
195+
196+
When set for the `npm init` command, this may be set to the folder of a
197+
workspace which does not yet exist, to create the folder and set it up as a
198+
brand new workspace within the project.
199+
200+
This value is not exported to the environment for child processes.
201+
202+
#### `workspaces`
203+
204+
* Default: false
205+
* Type: Boolean
206+
207+
Enable running a command in the context of **all** the configured
208+
workspaces.
209+
210+
This value is not exported to the environment for child processes.
211+
180212
<!-- AUTOGENERATED CONFIG DESCRIPTIONS END -->
181213

182214
### See Also

lib/ls.js

+29-5
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ const _parent = Symbol('parent')
2020
const _problems = Symbol('problems')
2121
const _required = Symbol('required')
2222
const _type = Symbol('type')
23-
const BaseCommand = require('./base-command.js')
23+
const ArboristWorkspaceCmd = require('./workspaces/arborist-cmd.js')
2424

25-
class LS extends BaseCommand {
25+
class LS extends ArboristWorkspaceCmd {
2626
/* istanbul ignore next - see test/lib/load-all-commands.js */
2727
static get description () {
2828
return 'List installed packages'
@@ -50,6 +50,7 @@ class LS extends BaseCommand {
5050
'omit',
5151
'link',
5252
'unicode',
53+
...super.params,
5354
]
5455
}
5556

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

92+
// filters by workspaces nodes when using -w <workspace-name>
93+
// We only have to filter the first layer of edges, so we don't
94+
// explore anything that isn't part of the selected workspace set.
95+
let wsNodes
96+
if (this.workspaces && this.workspaces.length)
97+
wsNodes = arb.workspaceNodes(tree, this.workspaces)
98+
const filterBySelectedWorkspaces = edge => {
99+
if (!wsNodes || !wsNodes.length)
100+
return true
101+
102+
if (edge.from.isProjectRoot) {
103+
return edge.to &&
104+
edge.to.isWorkspace &
105+
wsNodes.includes(edge.to.target)
106+
}
107+
108+
return true
109+
}
110+
91111
const seenItems = new Set()
92112
const seenNodes = new Map()
93113
const problems = new Set()
@@ -109,11 +129,14 @@ class LS extends BaseCommand {
109129
// `nodeResult` is going to be the returned `item` from `visit`
110130
getChildren (node, nodeResult) {
111131
const seenPaths = new Set()
132+
const workspace = node.isWorkspace
133+
const currentDepth = workspace ? 0 : node[_depth]
112134
const shouldSkipChildren =
113-
!(node instanceof Arborist.Node) || (node[_depth] > depthToPrint)
135+
!(node instanceof Arborist.Node) || (currentDepth > depthToPrint)
114136
return (shouldSkipChildren)
115137
? []
116138
: [...(node.target || node).edgesOut.values()]
139+
.filter(filterBySelectedWorkspaces)
117140
.filter(filterByEdgesTypes({
118141
dev,
119142
development,
@@ -129,7 +152,7 @@ class LS extends BaseCommand {
129152
.sort(sortAlphabetically)
130153
.map(augmentNodesWithMetadata({
131154
args,
132-
currentDepth: node[_depth],
155+
currentDepth,
133156
nodeResult,
134157
seenNodes,
135158
}))
@@ -257,7 +280,8 @@ const augmentItemWithIncludeMetadata = (node, item) => {
257280

258281
const getHumanOutputItem = (node, { args, color, global, long }) => {
259282
const { pkgid, path } = node
260-
let printable = pkgid
283+
const workspacePkgId = color ? chalk.green(pkgid) : pkgid
284+
let printable = node.isWorkspace ? workspacePkgId : pkgid
261285

262286
// special formatting for top-level package name
263287
if (node.isRoot) {

tap-snapshots/test/lib/load-all-commands.js.test.cjs

+4
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,8 @@ Options:
539539
[-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth <depth>]
540540
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]] [--link]
541541
[--unicode]
542+
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
543+
[-ws|--workspaces]
542544
543545
alias: la
544546
@@ -587,6 +589,8 @@ Options:
587589
[-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth <depth>]
588590
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]] [--link]
589591
[--unicode]
592+
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
593+
[-ws|--workspaces]
590594
591595
alias: list
592596

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

+51-5
Original file line numberDiff line numberDiff line change
@@ -478,17 +478,63 @@ exports[`test/lib/ls.js TAP ls json read problems > should print empty result 1`
478478
479479
`
480480

481+
exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should filter by parent folder workspace config 1`] = `
482+
[email protected] {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
483+
+-- [email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/group/e
484+
\`-- [email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/group/f
485+
486+
`
487+
481488
exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should filter single workspace 1`] = `
482-
[email protected] {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
489+
[email protected] {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
490+
+-- [email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/a
491+
| \`-- [email protected] deduped -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
492+
\`-- [email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
493+
494+
`
495+
496+
exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should filter using workspace config 1`] = `
497+
[email protected] {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
483498
\`-- [email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/a
499+
500+
\`-- [email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
501+
502+
484503
485504
`
486505

487-
exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should list workspaces properly 1`] = `
488-
filter-by-child-of-missing-dep@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
506+
exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should list --all workspaces properly 1`] = `
507+
workspaces-tree@1.0.0 {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
489508
+-- [email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/a
490-
491-
\`-- [email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/b
509+
510+
| \`-- [email protected] deduped -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
511+
+-- [email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/b
512+
+-- [email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
513+
514+
515+
+-- [email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/group/e
516+
\`-- [email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/group/f
517+
518+
`
519+
520+
exports[`test/lib/ls.js TAP ls loading a tree containing workspaces > should list workspaces properly with default configs 1`] = `
521+
[[email protected] {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
522+
+-- [[email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/a
523+
| +-- [email protected]
524+
| \`-- [email protected] deduped -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
525+
+-- [[email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/b
526+
+-- [[email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
527+
| \`-- [email protected]
528+
+-- [[email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/group/e
529+
\`-- [[email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/group/f
530+

531+
`
532+
533+
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`] = `
534+
[email protected] {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces
535+
\`-- [email protected] -> {CWD}/tap-testdir-ls-ls-loading-a-tree-containing-workspaces/d
536+
537+
492538
493539
`
494540

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

+4
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,8 @@ All commands:
640640
[-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth <depth>]
641641
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]] [--link]
642642
[--unicode]
643+
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
644+
[-ws|--workspaces]
643645
644646
alias: la
645647
@@ -682,6 +684,8 @@ All commands:
682684
[-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth <depth>]
683685
[--omit <dev|optional|peer> [--omit <dev|optional|peer> ...]] [--link]
684686
[--unicode]
687+
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
688+
[-ws|--workspaces]
685689
686690
alias: list
687691

test/lib/ls.js

+114-10
Original file line numberDiff line numberDiff line change
@@ -1407,14 +1407,16 @@ t.test('ls', (t) => {
14071407
})
14081408
})
14091409

1410-
t.test('loading a tree containing workspaces', (t) => {
1411-
npm.prefix = t.testdir({
1410+
t.test('loading a tree containing workspaces', async (t) => {
1411+
npm.localPrefix = npm.prefix = t.testdir({
14121412
'package.json': JSON.stringify({
1413-
name: 'filter-by-child-of-missing-dep',
1413+
name: 'workspaces-tree',
14141414
version: '1.0.0',
14151415
workspaces: [
14161416
'./a',
14171417
'./b',
1418+
'./d',
1419+
'./group/*',
14181420
],
14191421
}),
14201422
node_modules: {
@@ -1426,13 +1428,29 @@ t.test('ls', (t) => {
14261428
version: '1.0.0',
14271429
}),
14281430
},
1431+
d: t.fixture('symlink', '../d'),
1432+
e: t.fixture('symlink', '../group/e'),
1433+
f: t.fixture('symlink', '../group/f'),
1434+
foo: {
1435+
'package.json': JSON.stringify({
1436+
name: 'foo',
1437+
version: '1.1.1',
1438+
dependencies: {
1439+
bar: '^1.0.0',
1440+
},
1441+
}),
1442+
},
1443+
bar: {
1444+
'package.json': JSON.stringify({ name: 'bar', version: '1.0.0' }),
1445+
},
14291446
},
14301447
a: {
14311448
'package.json': JSON.stringify({
14321449
name: 'a',
14331450
version: '1.0.0',
14341451
dependencies: {
14351452
c: '^1.0.0',
1453+
d: '^1.0.0',
14361454
},
14371455
}),
14381456
},
@@ -1442,18 +1460,104 @@ t.test('ls', (t) => {
14421460
version: '1.0.0',
14431461
}),
14441462
},
1463+
d: {
1464+
'package.json': JSON.stringify({
1465+
name: 'd',
1466+
version: '1.0.0',
1467+
dependencies: {
1468+
foo: '^1.1.1',
1469+
},
1470+
}),
1471+
},
1472+
group: {
1473+
e: {
1474+
'package.json': JSON.stringify({
1475+
name: 'e',
1476+
version: '1.0.0',
1477+
}),
1478+
},
1479+
f: {
1480+
'package.json': JSON.stringify({
1481+
name: 'f',
1482+
version: '1.0.0',
1483+
}),
1484+
},
1485+
},
14451486
})
14461487

1447-
ls.exec([], (err) => {
1448-
t.error(err, 'should NOT have ELSPROBLEMS error code')
1449-
t.matchSnapshot(redactCwd(result), 'should list workspaces properly')
1488+
await new Promise((res, rej) => {
1489+
config.all = false
1490+
config.depth = 0
1491+
npm.color = true
1492+
ls.exec([], (err) => {
1493+
if (err)
1494+
rej(err)
1495+
1496+
t.matchSnapshot(redactCwd(result),
1497+
'should list workspaces properly with default configs')
1498+
config.all = true
1499+
config.depth = Infinity
1500+
npm.color = false
1501+
res()
1502+
})
1503+
})
1504+
1505+
// --all
1506+
await new Promise((res, rej) => {
1507+
ls.exec([], (err) => {
1508+
if (err)
1509+
rej(err)
1510+
1511+
t.matchSnapshot(redactCwd(result),
1512+
'should list --all workspaces properly')
1513+
res()
1514+
})
1515+
})
1516+
1517+
// filter out a single workspace using args
1518+
await new Promise((res, rej) => {
1519+
ls.exec(['d'], (err) => {
1520+
if (err)
1521+
rej(err)
14501522

1451-
// should also be able to filter out one of the workspaces
1452-
ls.exec(['a'], (err) => {
1453-
t.error(err, 'should NOT have ELSPROBLEMS error code when filter')
14541523
t.matchSnapshot(redactCwd(result), 'should filter single workspace')
1524+
res()
1525+
})
1526+
})
1527+
1528+
// filter out a single workspace and its deps using workspaces filters
1529+
await new Promise((res, rej) => {
1530+
ls.execWorkspaces([], ['a'], (err) => {
1531+
if (err)
1532+
rej(err)
1533+
1534+
t.matchSnapshot(redactCwd(result),
1535+
'should filter using workspace config')
1536+
res()
1537+
})
1538+
})
1539+
1540+
// filter out a workspace by parent path
1541+
await new Promise((res, rej) => {
1542+
ls.execWorkspaces([], ['./group'], (err) => {
1543+
if (err)
1544+
rej(err)
1545+
1546+
t.matchSnapshot(redactCwd(result),
1547+
'should filter by parent folder workspace config')
1548+
res()
1549+
})
1550+
})
1551+
1552+
// filter by a dep within a workspaces sub tree
1553+
await new Promise((res, rej) => {
1554+
ls.execWorkspaces(['bar'], ['d'], (err) => {
1555+
if (err)
1556+
rej(err)
14551557

1456-
t.end()
1558+
t.matchSnapshot(redactCwd(result),
1559+
'should print all tree and filter by dep within only the ws subtree')
1560+
res()
14571561
})
14581562
})
14591563
})

0 commit comments

Comments
 (0)