diff --git a/doc/cli/npm-dedupe.md b/doc/cli/npm-dedupe.md index d68832145f0a5..f193660da5630 100644 --- a/doc/cli/npm-dedupe.md +++ b/doc/cli/npm-dedupe.md @@ -43,10 +43,10 @@ be deleted. Arguments are ignored. Dedupe always acts on the entire tree. -Modules +### Modules -Note that this operation transforms the dependency tree, but will never -result in new modules being installed. +Note that this operation automatically fixes broken installs, which can sometimes +cause the installation of new packages, just like `npm-install(1)` does. ## SEE ALSO diff --git a/lib/dedupe.js b/lib/dedupe.js index 325faeaabcd43..9ed17f0f6b988 100644 --- a/lib/dedupe.js +++ b/lib/dedupe.js @@ -59,6 +59,10 @@ Deduper.prototype.loadIdealTree = function (cb) { [this, this.cloneCurrentTreeToIdealTree], [this, this.finishTracker, 'cloneCurrentTree'], + [this.newTracker(this.progress.loadIdealTree, 'loadIdealTree:loadShrinkwrap')], + [this, this.loadShrinkwrap], + [this, this.finishTracker, 'loadIdealTree:loadShrinkwrap'], + [this.newTracker(this.progress.loadIdealTree, 'loadAllDepsIntoIdealTree', 10)], [ function (next) { loadExtraneous(self.idealTree, self.progress.loadAllDepsIntoIdealTree, next) diff --git a/test/tap/dedupe-optional.js b/test/tap/dedupe-optional.js new file mode 100644 index 0000000000000..a4a7a4fe72f26 --- /dev/null +++ b/test/tap/dedupe-optional.js @@ -0,0 +1,95 @@ +'use strict' +const fs = require('fs') +const path = require('path') +const test = require('tap').test +const mr = require('npm-registry-mock') +const Tacks = require('tacks') +const File = Tacks.File +const Dir = Tacks.Dir +const common = require('../common-tap.js') + +const basedir = path.join(__dirname, path.basename(__filename, '.js')) +const testdir = path.join(basedir, 'testdir') +const cachedir = path.join(basedir, 'cache') +const globaldir = path.join(basedir, 'global') +const tmpdir = path.join(basedir, 'tmp') + +const conf = { + cwd: testdir, + env: Object.assign({}, process.env, { + npm_config_cache: cachedir, + npm_config_tmp: tmpdir, + npm_config_prefix: globaldir, + npm_config_registry: common.registry, + npm_config_loglevel: 'warn' + }) +} + +let server +const fixture = new Tacks(Dir({ + cache: Dir(), + global: Dir(), + tmp: Dir(), + testdir: Dir({ + 'package-lock.json': File({ + name: 'dedupe-optional', + version: '1.0.0', + lockfileVersion: 1, + requires: true, + dependencies: { + async: { + version: '0.9.2', + resolved: 'https://registry.npmjs.org/async/-/async-0.9.2.tgz', + integrity: 'sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=', + optional: true + } + } + }), + 'package.json': File({ + name: 'dedupe-optional', + version: '1.0.0', + optionalDependencies: { + async: '*' + } + }) + }) +})) + +test('setup', function (t) { + setup() + mr({port: common.port, throwOnUnmatched: true}, function (err, s) { + if (err) throw err + server = s + t.done() + }) +}) + +test('dedupe keeps uninstalled packages in package-lock.json', function (t) { + t.comment('test for https://npm.community/t/3807') + common.npm(['dedupe'], conf, function (err, code) { + if (err) throw err + t.is(code, 0, 'command ran ok') + + const shrinkwrap = JSON.parse( + fs.readFileSync( + path.join(testdir, 'package-lock.json'), 'utf8')) + + t.ok(shrinkwrap.dependencies, 'npm dedupe kept packages') + t.done() + }) +}) + +test('cleanup', function (t) { + server.close() + cleanup() + t.done() +}) + +function cleanup () { + fixture.remove(basedir) +} + +function setup () { + cleanup() + fixture.create(basedir) +}