Skip to content

Commit 5e49bda

Browse files
committed
Add 'npm explain' command
Pass a specifier or folder path, and it'll explain what that dependency is doing there. PR-URL: #1776 Credit: @isaacs Close: #1776 Reviewed-by: @ruyadorno
1 parent 7418970 commit 5e49bda

File tree

8 files changed

+366
-6
lines changed

8 files changed

+366
-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+
30+
node_modules/glob
31+
glob@"^7.1.4" from the root project
32+
33+
34+
node_modules/tacks/node_modules/glob
35+
glob@"^7.0.5" from [email protected]
36+
node_modules/tacks/node_modules/rimraf
37+
rimraf@"^2.6.2" from [email protected]
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+
50+
node_modules/nyc/node_modules/find-up
51+
find-up@"^3.0.0" from [email protected]
52+
node_modules/nyc
53+
nyc@"^14.1.1" from [email protected]
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: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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+
const validName = require('validate-npm-package-name')
11+
12+
const usage = usageUtil('explain', 'npm explain <folder | specifier>')
13+
14+
const cmd = (args, cb) => explain(args).then(() => cb()).catch(cb)
15+
16+
const explain = async (args) => {
17+
if (!args.length) {
18+
throw usage
19+
}
20+
21+
const arb = new Arborist({ path: npm.prefix, ...npm.flatOptions })
22+
const tree = await arb.loadActual()
23+
24+
const nodes = new Set()
25+
for (const arg of args) {
26+
for (const node of getNodes(tree, arg)) {
27+
nodes.add(node)
28+
}
29+
}
30+
if (nodes.size === 0) {
31+
throw `No dependencies found matching ${args.join(', ')}`
32+
}
33+
34+
const expls = []
35+
for (const node of nodes) {
36+
const { extraneous, dev, optional, devOptional, peer } = node
37+
const expl = node.explain()
38+
if (extraneous) {
39+
expl.extraneous = true
40+
} else {
41+
expl.dev = dev
42+
expl.optional = optional
43+
expl.devOptional = devOptional
44+
expl.peer = peer
45+
}
46+
expls.push(expl)
47+
}
48+
49+
if (npm.flatOptions.json) {
50+
output(JSON.stringify(expls, null, 2))
51+
} else {
52+
output(expls.map(expl => {
53+
return explainNode(expl, Infinity, npm.color)
54+
}).join('\n\n'))
55+
}
56+
}
57+
58+
const getNodes = (tree, arg) => {
59+
// if it's just a name, return packages by that name
60+
const { validForOldPackages: valid } = validName(arg)
61+
if (valid) {
62+
return tree.inventory.query('name', arg)
63+
}
64+
65+
// if it's a location, get that node
66+
const maybeLoc = arg.replace(/\\/g, '/').replace(/\/+$/, '')
67+
const nodeByLoc = tree.inventory.get(maybeLoc)
68+
if (nodeByLoc) {
69+
return [nodeByLoc]
70+
}
71+
72+
// maybe a path to a node_modules folder
73+
const maybePath = relative(npm.prefix, resolve(maybeLoc))
74+
.replace(/\\/g, '/').replace(/\/+$/, '')
75+
const nodeByPath = tree.inventory.get(maybePath)
76+
if (nodeByPath) {
77+
return [nodeByPath]
78+
}
79+
80+
// otherwise, try to select all matching nodes
81+
try {
82+
return getNodesByVersion(tree, arg)
83+
} catch (er) {
84+
return []
85+
}
86+
}
87+
88+
const getNodesByVersion = (tree, arg) => {
89+
const spec = npa(arg, npm.prefix)
90+
if (spec.type !== 'version' && spec.type !== 'range') {
91+
return []
92+
}
93+
94+
return tree.inventory.filter(node => {
95+
return node.package.name === spec.name &&
96+
semver.satisfies(node.package.version, spec.rawSpec)
97+
})
98+
}
99+
100+
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)