Skip to content

Commit dbd5ea9

Browse files
committed
Add 'npm explain' command
Pass a specifier or folder path, and it'll explain what that dependency is doing there.
1 parent 7418970 commit dbd5ea9

File tree

8 files changed

+340
-6
lines changed

8 files changed

+340
-6
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
section: cli-commands
3+
title: npm-explain
4+
description: Explain installed packages
5+
---
6+
7+
# npm-explain(1)
8+
9+
## Explain installed packages
10+
11+
### Synopsis
12+
13+
```bash
14+
npm explain <folder | specifier>
15+
```
16+
17+
### Description
18+
19+
This command will print the chain of dependencies causing a given package
20+
to be installed in the current project.
21+
22+
Positional arguments can be either folders within `node_modules`, or
23+
`name@version-range` specifiers, which will select the dependency
24+
relationships to explain.
25+
26+
For example, running `npm explain glob` within npm's source tree will show:
27+
28+
```bash
29+
glob@7.1.6
30+
node_modules/glob
31+
glob@"^7.1.4" from the root project
32+
33+
glob@7.1.1 dev
34+
node_modules/tacks/node_modules/glob
35+
glob@"^7.0.5" from rimraf@2.6.2
36+
node_modules/tacks/node_modules/rimraf
37+
rimraf@"^2.6.2" from tacks@1.3.0
38+
node_modules/tacks
39+
dev tacks@"^1.3.0" from the root project
40+
```
41+
42+
To explain just the package residing at a specific folder, pass that as the
43+
argument to the command. This can be useful when trying to figure out
44+
exactly why a given dependency is being duplicated to satisfy conflicting
45+
version requirements within the project.
46+
47+
```bash
48+
$ npm explain node_modules/nyc/node_modules/find-up
49+
find-up@3.0.0 dev
50+
node_modules/nyc/node_modules/find-up
51+
find-up@"^3.0.0" from nyc@14.1.1
52+
node_modules/nyc
53+
nyc@"^14.1.1" from tap@14.10.8
54+
node_modules/tap
55+
dev tap@"^14.10.8" from the root project
56+
```
57+
58+
### Configuration
59+
60+
#### json
61+
62+
* Default: false
63+
* Type: Bolean
64+
65+
Show information in JSON format.
66+
67+
### See Also
68+
69+
* [npm config](/cli-commands/config)
70+
* [npmrc](/configuring-npm/npmrc)
71+
* [npm folders](/configuring-npm/folders)
72+
* [npm ls](/cli-commands/ls)
73+
* [npm install](/cli-commands/install)
74+
* [npm link](/cli-commands/link)
75+
* [npm prune](/cli-commands/prune)
76+
* [npm outdated](/cli-commands/outdated)
77+
* [npm update](/cli-commands/update)

docs/content/cli-commands/npm-ls.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
section: cli-commands
2+
section: cli-commands
33
title: npm-ls
44
description: List installed packages
55
---
@@ -122,6 +122,7 @@ Set it to false in order to use all-ansi output.
122122
* [npm config](/cli-commands/config)
123123
* [npmrc](/configuring-npm/npmrc)
124124
* [npm folders](/configuring-npm/folders)
125+
* [npm explain](/cli-commands/explain)
125126
* [npm install](/cli-commands/install)
126127
* [npm link](/cli-commands/link)
127128
* [npm prune](/cli-commands/prune)

lib/explain.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const usageUtil = require('./utils/usage.js')
2+
const npm = require('./npm.js')
3+
const { explainNode } = require('./utils/explain-dep.js')
4+
const completion = require('./utils/completion/installed-deep.js')
5+
const output = require('./utils/output.js')
6+
const Arborist = require('@npmcli/arborist')
7+
const npa = require('npm-package-arg')
8+
const semver = require('semver')
9+
const { relative, resolve } = require('path')
10+
11+
const usage = usageUtil('explain', 'npm explain <folder | specifier>')
12+
13+
const cmd = (args, cb) => explain(args).then(() => cb()).catch(cb)
14+
15+
const explain = async (args) => {
16+
if (!args.length)
17+
throw usage
18+
19+
const arb = new Arborist({ path: npm.prefix, ...npm.flatOptions })
20+
const tree = await arb.loadActual()
21+
22+
const nodes = new Set()
23+
for (const arg of args) {
24+
for (const node of getNodes(tree, arg)) {
25+
nodes.add(node)
26+
}
27+
}
28+
if (nodes.size === 0) {
29+
throw `No dependencies found matching ${args.join(', ')}`
30+
}
31+
32+
const expls = []
33+
for (const node of nodes) {
34+
const { extraneous, dev, optional, devOptional, peer } = node
35+
const expl = node.explain()
36+
if (extraneous) {
37+
expl.extraneous = true
38+
} else {
39+
expl.dev = dev
40+
expl.optional = optional
41+
expl.devOptional = devOptional
42+
expl.peer = peer
43+
}
44+
expls.push(expl)
45+
}
46+
47+
if (npm.flatOptions.json)
48+
output(JSON.stringify(expls, null, 2))
49+
else
50+
output(expls.map(expl => {
51+
return explainNode(expl, Infinity, npm.color)
52+
}).join('\n\n'))
53+
}
54+
55+
const getNodes = (tree, arg) => {
56+
// if it's just a name, return packages by that name
57+
if (/^(@[^/]+\/)?[^/@]+$/.test(arg)) {
58+
return tree.inventory.query('name', arg)
59+
}
60+
// if it's a location, get that node
61+
const maybeLoc = arg.replace(/\\/g, '/').replace(/\/+$/, '')
62+
const nodeByLoc = tree.inventory.get(maybeLoc)
63+
if (nodeByLoc) {
64+
return [nodeByLoc]
65+
}
66+
67+
// maybe a different kind of absolute path?
68+
const maybePath = relative(npm.prefix, resolve(maybeLoc))
69+
.replace(/\\/g, '/').replace(/\/+$/, '')
70+
const nodeByPath = tree.inventory.get(maybePath)
71+
if (nodeByPath) {
72+
return [nodeByPath]
73+
}
74+
75+
// otherwise, try to select all matching nodes
76+
return getNodesByVersion(tree, arg)
77+
}
78+
79+
const getNodesByVersion = (tree, arg) => {
80+
const spec = npa(arg, npm.prefix)
81+
if (spec.type !== 'version' && spec.type !== 'range') {
82+
return []
83+
}
84+
85+
return tree.inventory.filter(node => {
86+
return node.package.name === spec.name &&
87+
semver.satisfies(node.package.version, spec.rawSpec)
88+
})
89+
}
90+
91+
module.exports = Object.assign(cmd, { usage, completion })

lib/utils/cmd-list.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ const shorthands = {
2020
run: 'run-script',
2121
'clean-install': 'ci',
2222
'clean-install-test': 'cit',
23-
x: 'exec'
23+
x: 'exec',
24+
why: 'explain'
2425
}
2526

2627
const affordances = {
@@ -128,7 +129,8 @@ const cmdList = [
128129
'run-script',
129130
'completion',
130131
'doctor',
131-
'exec'
132+
'exec',
133+
'explain'
132134
]
133135

134136
const plumbing = ['birthday', 'help-search']

lib/utils/explain-dep.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ const explainDependents = ({ name, dependents }, depth, color) => {
6363
// show just the names of the first 5 deps that overflowed the list
6464
if (dependents.length > max) {
6565
let len = 0
66-
const maxLen = 30
66+
const maxLen = 50
6767
const showNames = []
6868
for (let i = max; i < dependents.length; i++) {
6969
const { from: { name } } = dependents[i]

tap-snapshots/test-lib-utils-cmd-list.js-TAP.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ Object {
100100
"urn": "run-script",
101101
"v": "view",
102102
"verison": "version",
103+
"why": "explain",
103104
"x": "exec",
104105
},
105106
"cmdList": Array [
@@ -164,6 +165,7 @@ Object {
164165
"completion",
165166
"doctor",
166167
"exec",
168+
"explain",
167169
],
168170
"plumbing": Array [
169171
"birthday",
@@ -190,6 +192,7 @@ Object {
190192
"unstar": "star",
191193
"up": "update",
192194
"v": "view",
195+
"why": "explain",
193196
"x": "exec",
194197
},
195198
}

tap-snapshots/test-lib-utils-explain-eresolve.js-TAP.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ Found: react@16.13.1
170170
peer react@"^16.4.2" from gatsby@2.24.53
171171
node_modules/gatsby
172172
gatsby@"" from the root project
173-
26 more (react-dom, @reach/router, gatsby-cli, ...)
173+
26 more (react-dom, @reach/router, gatsby-cli, gatsby-link, ...)
174174
175175
Could not add conflicting dependency: react@16.8.1
176176
node_modules/react
@@ -734,7 +734,7 @@ Found: react@16.13.1
734734
peer react@"^16.4.2" from gatsby@2.24.53
735735
node_modules/gatsby
736736
gatsby@"" from the root project
737-
26 more (react-dom, @reach/router, gatsby-cli, ...)
737+
26 more (react-dom, @reach/router, gatsby-cli, gatsby-link, ...)
738738
739739
Could not add conflicting dependency: react@16.8.1
740740
node_modules/react

0 commit comments

Comments
 (0)