Skip to content

Commit aa82717

Browse files
jviottizkat
authored andcommitted
install: Don't omit any deps of dev deps if --only=dev (#69)
* install: Don't omit any deps of dev deps if --only=dev At the moment, npm will NOT install a dependency that is both a production and a development dependency in the same tree if `--only=dev`. Consider the following scenario: - `package1` has `prod-dependency` in `dependencies` - `package1` has `dev-dependency` in `devDependencies` - `prod-dependency` has `sub-dependency` in `dependencies` - `dev-dependency` has `sub-dependency` in `dependencies` So both `prod-dependency` and `dev-dependency` directly depend on `sub-dependency`. Since `sub-dependency` is required by at least one production dependency, then npm won't consider it as "only a dev dependency", and therefore running `npm install --only=dev` will result in the following tree: ``` package1/ node_modules/ dev-dependency ``` Notice that `sub-dependency` has ben completely omitted from the tree, even though `dev-dependency` clearly requires it, and therefore it will not work. This commit makes `--only=dev` always install required dependencies of dev dependencies, so you never end up with a broken tree. For a more real-world reproducible example, try to run `npm install --only=dev` in https://github.com/resin-io/etcher/tree/a338c6e60a10aad00008febfe92e2556dcf586c6. The resulting tree will be completely broken, but we would not have identified this if several install scripts wouldn't fail as a result. PR-URL: #69 Credit: @jviotti Reviewed-By: @iarna
1 parent de0ebe1 commit aa82717

File tree

3 files changed

+46
-2
lines changed

3 files changed

+46
-2
lines changed

lib/install/diff-trees.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var validate = require('aproba')
44
var npa = require('npm-package-arg')
55
var flattenTree = require('./flatten-tree.js')
66
var isOnlyDev = require('./is-only-dev.js')
7+
var isDev = require('./is-dev.js')
78
var log = require('npmlog')
89
var path = require('path')
910
var ssri = require('ssri')
@@ -250,10 +251,11 @@ function filterActions (differences) {
250251
log.silly('diff-trees', 'filtering actions:', 'includeDev', includeDev, 'includeProd', includeProd, 'includeOpt', includeOpt)
251252
return differences.filter((diff) => {
252253
const pkg = diff[1]
254+
const pkgIsDev = isDev(pkg)
253255
const pkgIsOnlyDev = isOnlyDev(pkg)
254256
const pkgIsOnlyOpt = isOnlyOptional(pkg)
255257
if (!includeProd && pkgIsOnlyDev) return true
256-
if (includeDev && pkgIsOnlyDev) return true
258+
if (includeDev && pkgIsDev) return true // if we used isOnlyDev here we'd exclude mixed prod/dev packages
257259
if (includeProd && !pkgIsOnlyDev && (includeOpt || !pkgIsOnlyOpt)) return true
258260
return false
259261
})

lib/install/is-dev.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict'
2+
module.exports = isDev
3+
4+
// Returns true if the module `node` is a dev dependency itself or a
5+
// dependency of some dev dependency higher in the hierarchy.
6+
function isDev (node, seen) {
7+
if (!seen) seen = new Set()
8+
if (seen.has(node.package.name)) return true
9+
10+
seen.add(node.package.name)
11+
if (!node.requiredBy || node.requiredBy.length === 0 || node.package._development || node.isTop) {
12+
return !!node.package._development
13+
}
14+
15+
return node.requiredBy.some((parent) => {
16+
return isDev(parent, seen)
17+
})
18+
}

test/tap/install-cli-only-development.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,24 @@ var json = {
2828
var dependency = {
2929
name: 'dependency',
3030
description: 'fixture',
31-
version: '0.0.0'
31+
version: '0.0.0',
32+
dependencies: {
33+
'sub-dependency': 'file:../sub-dependency'
34+
}
3235
}
3336

3437
var devDependency = {
3538
name: 'dev-dependency',
3639
description: 'fixture',
40+
version: '0.0.0',
41+
dependencies: {
42+
'sub-dependency': 'file:../sub-dependency'
43+
}
44+
}
45+
46+
var subDependency = {
47+
name: 'sub-dependency',
48+
description: 'fixture',
3749
version: '0.0.0'
3850
}
3951

@@ -57,6 +69,12 @@ test('\'npm install --only=development\' should only install devDependencies', f
5769
existsSync(path.resolve(pkg, 'node_modules/dependency/package.json')),
5870
'dependency was NOT installed'
5971
)
72+
t.ok(
73+
JSON.parse(fs.readFileSync(
74+
path.resolve(pkg, 'node_modules/sub-dependency/package.json'), 'utf8')
75+
),
76+
'subDependency was installed'
77+
)
6078
t.end()
6179
})
6280
})
@@ -101,6 +119,12 @@ function setup () {
101119
JSON.stringify(devDependency, null, 2)
102120
)
103121

122+
mkdirp.sync(path.join(pkg, 'sub-dependency'))
123+
fs.writeFileSync(
124+
path.join(pkg, 'sub-dependency', 'package.json'),
125+
JSON.stringify(subDependency, null, 2)
126+
)
127+
104128
mkdirp.sync(path.join(pkg, 'node_modules'))
105129
fs.writeFileSync(
106130
path.join(pkg, 'package.json'),

0 commit comments

Comments
 (0)