Skip to content

Commit

Permalink
install: Don't omit any deps of dev deps if --only=dev (#69)
Browse files Browse the repository at this point in the history
* 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: npm#69
Credit: @jviotti
Reviewed-By: @iarna
  • Loading branch information
jviotti authored and petershin committed Mar 20, 2019
1 parent 656bce7 commit 4fb3465
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 2 deletions.
4 changes: 3 additions & 1 deletion lib/install/diff-trees.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var validate = require('aproba')
var npa = require('npm-package-arg')
var flattenTree = require('./flatten-tree.js')
var isOnlyDev = require('./is-only-dev.js')
var isDev = require('./is-dev.js')
var log = require('npmlog')
var path = require('path')
var ssri = require('ssri')
Expand Down Expand Up @@ -250,10 +251,11 @@ function filterActions (differences) {
log.silly('diff-trees', 'filtering actions:', 'includeDev', includeDev, 'includeProd', includeProd, 'includeOpt', includeOpt)
return differences.filter((diff) => {
const pkg = diff[1]
const pkgIsDev = isDev(pkg)
const pkgIsOnlyDev = isOnlyDev(pkg)
const pkgIsOnlyOpt = isOnlyOptional(pkg)
if (!includeProd && pkgIsOnlyDev) return true
if (includeDev && pkgIsOnlyDev) return true
if (includeDev && pkgIsDev) return true // if we used isOnlyDev here we'd exclude mixed prod/dev packages
if (includeProd && !pkgIsOnlyDev && (includeOpt || !pkgIsOnlyOpt)) return true
return false
})
Expand Down
18 changes: 18 additions & 0 deletions lib/install/is-dev.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict'
module.exports = isDev

// Returns true if the module `node` is a dev dependency itself or a
// dependency of some dev dependency higher in the hierarchy.
function isDev (node, seen) {
if (!seen) seen = new Set()
if (seen.has(node.package.name)) return true

seen.add(node.package.name)
if (!node.requiredBy || node.requiredBy.length === 0 || node.package._development || node.isTop) {
return !!node.package._development
}

return node.requiredBy.some((parent) => {
return isDev(parent, seen)
})
}
26 changes: 25 additions & 1 deletion test/tap/install-cli-only-development.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,24 @@ var json = {
var dependency = {
name: 'dependency',
description: 'fixture',
version: '0.0.0'
version: '0.0.0',
dependencies: {
'sub-dependency': 'file:../sub-dependency'
}
}

var devDependency = {
name: 'dev-dependency',
description: 'fixture',
version: '0.0.0',
dependencies: {
'sub-dependency': 'file:../sub-dependency'
}
}

var subDependency = {
name: 'sub-dependency',
description: 'fixture',
version: '0.0.0'
}

Expand All @@ -57,6 +69,12 @@ test('\'npm install --only=development\' should only install devDependencies', f
existsSync(path.resolve(pkg, 'node_modules/dependency/package.json')),
'dependency was NOT installed'
)
t.ok(
JSON.parse(fs.readFileSync(
path.resolve(pkg, 'node_modules/sub-dependency/package.json'), 'utf8')
),
'subDependency was installed'
)
t.end()
})
})
Expand Down Expand Up @@ -101,6 +119,12 @@ function setup () {
JSON.stringify(devDependency, null, 2)
)

mkdirp.sync(path.join(pkg, 'sub-dependency'))
fs.writeFileSync(
path.join(pkg, 'sub-dependency', 'package.json'),
JSON.stringify(subDependency, null, 2)
)

mkdirp.sync(path.join(pkg, 'node_modules'))
fs.writeFileSync(
path.join(pkg, 'package.json'),
Expand Down

0 comments on commit 4fb3465

Please # to comment.