From 1258630faa1d0d4a7eb4abe4eac0a66651477842 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Thu, 7 Mar 2019 16:37:07 +0100
Subject: [PATCH] feat(npm): dedupe (#3322)

Allows dedupe options for npm and yarn.

Closes #2883
---
 lib/config/definitions.js                   |  9 ++++--
 lib/manager/npm/post-update/index.js        |  3 +-
 lib/manager/npm/post-update/npm.js          | 34 +++++++++++++++------
 lib/manager/npm/post-update/yarn.js         | 34 +++++++++++++++++++--
 test/workers/branch/lock-files/npm.spec.js  | 13 +++++---
 test/workers/branch/lock-files/yarn.spec.js | 14 ++++++++-
 website/docs/configuration-options.md       |  5 ++-
 7 files changed, 89 insertions(+), 23 deletions(-)

diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index 069eece75a..4b9dc1ff5f 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -1080,10 +1080,15 @@ const options = [
   {
     name: 'postUpdateOptions',
     description:
-      'Enable various post-update options to be run after package/artifact updating',
+      'Enable post-update options to be run after package/artifact updating',
     type: 'list',
     default: [],
-    allowedValues: ['gomodTidy'],
+    allowedValues: [
+      'gomodTidy',
+      'npmDedupe',
+      'yarnDedupeFewer',
+      'yarnDedupeHighest',
+    ],
     cli: false,
     env: false,
     mergeable: true,
diff --git a/lib/manager/npm/post-update/index.js b/lib/manager/npm/post-update/index.js
index e26868e8b1..720f235b2f 100644
--- a/lib/manager/npm/post-update/index.js
+++ b/lib/manager/npm/post-update/index.js
@@ -378,8 +378,7 @@ async function getAdditionalFiles(config, packageFiles) {
       upath.join(config.localDir, lockFileDir),
       env,
       fileName,
-      config.skipInstalls,
-      config.binarySource,
+      config,
       upgrades
     );
     if (res.error) {
diff --git a/lib/manager/npm/post-update/npm.js b/lib/manager/npm/post-update/npm.js
index 4469042a4d..9c6dc96d06 100644
--- a/lib/manager/npm/post-update/npm.js
+++ b/lib/manager/npm/post-update/npm.js
@@ -11,15 +11,16 @@ async function generateLockFile(
   cwd,
   env,
   filename,
-  skipInstalls,
-  binarySource,
+  config = {},
   upgrades = []
 ) {
   logger.debug(`Spawning npm install to create ${cwd}/${filename}`);
+  const { skipInstalls, binarySource, postUpdateOptions } = config;
   let lockFile = null;
   let stdout;
   let stderr;
   let cmd;
+  let args = '';
   try {
     const startTime = process.hrtime();
     try {
@@ -62,15 +63,15 @@ async function generateLockFile(
     if (binarySource === 'global') {
       cmd = 'npm';
     }
-    cmd = `${cmd} --version && ${cmd} install`;
+    args = `install`;
     if (skipInstalls) {
-      cmd += ' --package-lock-only --no-audit';
+      args += ' --package-lock-only --no-audit';
     } else {
-      cmd += ' --ignore-scripts --no-audit';
+      args += ' --ignore-scripts --no-audit';
     }
-    logger.debug(`Using npm: ${cmd}`);
+    logger.debug(`Using npm: ${cmd} ${args}`);
     // TODO: Switch to native util.promisify once using only node 8
-    ({ stdout, stderr } = await exec(cmd, {
+    ({ stdout, stderr } = await exec(`${cmd} ${args}`, {
       cwd,
       shell: true,
       env,
@@ -81,15 +82,27 @@ async function generateLockFile(
     if (lockUpdates.length) {
       logger.info('Performing lockfileUpdate (npm)');
       const updateCmd =
-        cmd +
+        `${cmd} ${args}` +
         lockUpdates
           .map(update => ` ${update.depName}@${update.toVersion}`)
           .join('');
-      ({ stdout, stderr } = await exec(updateCmd, {
+      const updateRes = await exec(updateCmd, {
         cwd,
         shell: true,
         env,
-      }));
+      });
+      stdout += updateRes.stdout ? updateRes.stdout : '';
+      stderr += updateRes.stderr ? updateRes.stderr : '';
+    }
+    if (postUpdateOptions && postUpdateOptions.includes('npmDedupe')) {
+      logger.info('Performing npm dedupe');
+      const dedupeRes = await exec(`${cmd} dedupe`, {
+        cwd,
+        shell: true,
+        env,
+      });
+      stdout += dedupeRes.stdout ? dedupeRes.stdout : '';
+      stderr += dedupeRes.stderr ? dedupeRes.stderr : '';
     }
     const duration = process.hrtime(startTime);
     const seconds = Math.round(duration[0] + duration[1] / 1e9);
@@ -102,6 +115,7 @@ async function generateLockFile(
     logger.info(
       {
         cmd,
+        args,
         err,
         stdout,
         stderr,
diff --git a/lib/manager/npm/post-update/yarn.js b/lib/manager/npm/post-update/yarn.js
index 821cad6fdf..361a3ab9ec 100644
--- a/lib/manager/npm/post-update/yarn.js
+++ b/lib/manager/npm/post-update/yarn.js
@@ -101,11 +101,41 @@ async function generateLockFile(cwd, env, config = {}, upgrades = []) {
         ' upgrade' +
         lockUpdates.map(depName => ` ${depName}`).join('') +
         cmdExtras;
-      ({ stdout, stderr } = await exec(updateCmd, {
+      const updateRes = await exec(updateCmd, {
         cwd,
         shell: true,
         env,
-      }));
+      });
+      stdout += updateRes.stdout ? updateRes.stdout : '';
+      stderr += updateRes.stderr ? updateRes.stderr : '';
+    }
+    if (
+      config.postUpdateOptions &&
+      config.postUpdateOptions.includes('yarnDedupeFewer')
+    ) {
+      logger.info('Performing yarn dedupe fewer');
+      const dedupeCommand = 'npx yarn-deduplicate@1.1.1 --strategy fewer';
+      const dedupeRes = await exec(dedupeCommand, {
+        cwd,
+        shell: true,
+        env,
+      });
+      stdout += dedupeRes.stdout ? dedupeRes.stdout : '';
+      stderr += dedupeRes.stderr ? dedupeRes.stderr : '';
+    }
+    if (
+      config.postUpdateOptions &&
+      config.postUpdateOptions.includes('yarnDedupeHighest')
+    ) {
+      logger.info('Performing yarn dedupe highest');
+      const dedupeCommand = 'npx yarn-deduplicate@1.1.1 --strategy highest';
+      const dedupeRes = await exec(dedupeCommand, {
+        cwd,
+        shell: true,
+        env,
+      });
+      stdout += dedupeRes.stdout ? dedupeRes.stdout : '';
+      stderr += dedupeRes.stderr ? dedupeRes.stderr : '';
     }
     const duration = process.hrtime(startTime);
     const seconds = Math.round(duration[0] + duration[1] / 1e9);
diff --git a/test/workers/branch/lock-files/npm.spec.js b/test/workers/branch/lock-files/npm.spec.js
index 1e36155ff5..404e3679bb 100644
--- a/test/workers/branch/lock-files/npm.spec.js
+++ b/test/workers/branch/lock-files/npm.spec.js
@@ -17,13 +17,18 @@ describe('generateLockFile', () => {
       stdout: '',
       stderror: '',
     });
+    exec.mockReturnValueOnce({
+      stdout: '',
+      stderror: '',
+    });
     fs.readFile = jest.fn(() => 'package-lock-contents');
     const skipInstalls = true;
+    const postUpdateOptions = ['npmDedupe'];
     const res = await npmHelper.generateLockFile(
       'some-dir',
       {},
       'package-lock.json',
-      skipInstalls
+      { skipInstalls, postUpdateOptions }
     );
     expect(fs.readFile.mock.calls.length).toEqual(1);
     expect(res.error).not.toBeDefined();
@@ -48,8 +53,7 @@ describe('generateLockFile', () => {
       'some-dir',
       {},
       'package-lock.json',
-      skipInstalls,
-      null,
+      { skipInstalls },
       updates
     );
     expect(fs.readFile.mock.calls.length).toEqual(1);
@@ -69,8 +73,7 @@ describe('generateLockFile', () => {
       'some-dir',
       {},
       'package-lock.json',
-      skipInstalls,
-      binarySource
+      { skipInstalls, binarySource }
     );
     expect(fs.readFile.mock.calls.length).toEqual(1);
     expect(res.error).not.toBeDefined();
diff --git a/test/workers/branch/lock-files/yarn.spec.js b/test/workers/branch/lock-files/yarn.spec.js
index a583500ff0..69f41b89f4 100644
--- a/test/workers/branch/lock-files/yarn.spec.js
+++ b/test/workers/branch/lock-files/yarn.spec.js
@@ -17,8 +17,20 @@ describe('generateLockFile', () => {
       stdout: '',
       stderror: '',
     });
+    exec.mockReturnValueOnce({
+      stdout: '',
+      stderror: '',
+    });
+    exec.mockReturnValueOnce({
+      stdout: '',
+      stderror: '',
+    });
     fs.readFile = jest.fn(() => 'package-lock-contents');
-    const res = await yarnHelper.generateLockFile('some-dir');
+    const env = {};
+    const config = {
+      postUpdateOptions: ['yarnDedupeFewer', 'yarnDedupeHighest'],
+    };
+    const res = await yarnHelper.generateLockFile('some-dir', env, config);
     expect(fs.readFile.mock.calls.length).toEqual(1);
     expect(res.lockFile).toEqual('package-lock-contents');
   });
diff --git a/website/docs/configuration-options.md b/website/docs/configuration-options.md
index a8400bb730..601416e335 100644
--- a/website/docs/configuration-options.md
+++ b/website/docs/configuration-options.md
@@ -670,7 +670,10 @@ Warning: 'pipenv' support is currently in beta, so it is not enabled by default.
 
 ## postUpdateOptions
 
-`gomodTidy`: Enable to run `go mod tidy` after Go module updates
+`gomodTidy`: Run `go mod tidy` after Go module updates
+`npmDedupe`: Run `npm dedupe` after `package-lock.json` updates
+`yarnDedupeFewer`: Run `yarn-deduplicate --strategy fewer` after `yarn.lock` updates
+`yarnDedupeHighest`: Run `yarn-deduplicate --strategy highest` after `yarn.lock` updates
 
 ## prBodyColumns
 
-- 
GitLab