From 53d376eb8d08aec5acbb16b8afad51177734fd17 Mon Sep 17 00:00:00 2001
From: Martin Herndl <martin.herndl@icis.com>
Date: Wed, 16 Mar 2022 10:17:47 +0100
Subject: [PATCH] fix(npm): massage lockfile with rangeStrategy=update-lockfile
 (#14586)

Co-authored-by: Rhys Arkins <rhys@arkins.net>
---
 .../package-lock.json                         |  83 +++++++++++++
 .../update-lockfile-massage-1/package.json    |   7 ++
 .../__snapshots__/npm.spec.ts.snap            | 109 ++++++++++++++++++
 .../manager/npm/post-update/npm.spec.ts       |  29 ++++-
 lib/modules/manager/npm/post-update/npm.ts    |  28 +++++
 5 files changed, 255 insertions(+), 1 deletion(-)
 create mode 100644 lib/modules/manager/npm/post-update/__fixtures__/update-lockfile-massage-1/package-lock.json
 create mode 100644 lib/modules/manager/npm/post-update/__fixtures__/update-lockfile-massage-1/package.json

diff --git a/lib/modules/manager/npm/post-update/__fixtures__/update-lockfile-massage-1/package-lock.json b/lib/modules/manager/npm/post-update/__fixtures__/update-lockfile-massage-1/package-lock.json
new file mode 100644
index 0000000000..3426f0d759
--- /dev/null
+++ b/lib/modules/manager/npm/post-update/__fixtures__/update-lockfile-massage-1/package-lock.json
@@ -0,0 +1,83 @@
+{
+  "name": "update-lockfile-massage-1",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "update-lockfile-massage-1",
+      "version": "1.0.0",
+      "dependencies": {
+        "postcss": "^8.4.8"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
+      "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+    },
+    "node_modules/postcss": {
+      "version": "8.4.8",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.8.tgz",
+      "integrity": "sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ==",
+      "dependencies": {
+        "nanoid": "^3.3.1",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    }
+  },
+  "dependencies": {
+    "nanoid": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
+      "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw=="
+    },
+    "picocolors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+    },
+    "postcss": {
+      "version": "8.4.8",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.8.tgz",
+      "integrity": "sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ==",
+      "requires": {
+        "nanoid": "^3.3.1",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      }
+    },
+    "source-map-js": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
+    }
+  }
+}
diff --git a/lib/modules/manager/npm/post-update/__fixtures__/update-lockfile-massage-1/package.json b/lib/modules/manager/npm/post-update/__fixtures__/update-lockfile-massage-1/package.json
new file mode 100644
index 0000000000..c4982dc8b8
--- /dev/null
+++ b/lib/modules/manager/npm/post-update/__fixtures__/update-lockfile-massage-1/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "update-lockfile-massage-1",
+  "version": "1.0.0",
+  "dependencies": {
+    "postcss": "^8.0.0"
+  }
+}
diff --git a/lib/modules/manager/npm/post-update/__snapshots__/npm.spec.ts.snap b/lib/modules/manager/npm/post-update/__snapshots__/npm.spec.ts.snap
index 0dcc2b29a2..6c274a61ef 100644
--- a/lib/modules/manager/npm/post-update/__snapshots__/npm.spec.ts.snap
+++ b/lib/modules/manager/npm/post-update/__snapshots__/npm.spec.ts.snap
@@ -93,6 +93,115 @@ Array [
 ]
 `;
 
+exports[`modules/manager/npm/post-update/npm performs lock file updates retaining the package.json counterparts 1`] = `
+"{
+  \\"name\\": \\"update-lockfile-massage-1\\",
+  \\"version\\": \\"1.0.0\\",
+  \\"lockfileVersion\\": 2,
+  \\"requires\\": true,
+  \\"packages\\": {
+    \\"\\": {
+      \\"name\\": \\"update-lockfile-massage-1\\",
+      \\"version\\": \\"1.0.0\\",
+      \\"dependencies\\": {
+        \\"postcss\\": \\"^8.0.0\\"
+      }
+    },
+    \\"node_modules/nanoid\\": {
+      \\"version\\": \\"3.3.1\\",
+      \\"resolved\\": \\"https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz\\",
+      \\"integrity\\": \\"sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==\\",
+      \\"bin\\": {
+        \\"nanoid\\": \\"bin/nanoid.cjs\\"
+      },
+      \\"engines\\": {
+        \\"node\\": \\"^10 || ^12 || ^13.7 || ^14 || >=15.0.1\\"
+      }
+    },
+    \\"node_modules/picocolors\\": {
+      \\"version\\": \\"1.0.0\\",
+      \\"resolved\\": \\"https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz\\",
+      \\"integrity\\": \\"sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==\\"
+    },
+    \\"node_modules/postcss\\": {
+      \\"version\\": \\"8.4.8\\",
+      \\"resolved\\": \\"https://registry.npmjs.org/postcss/-/postcss-8.4.8.tgz\\",
+      \\"integrity\\": \\"sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ==\\",
+      \\"dependencies\\": {
+        \\"nanoid\\": \\"^3.3.1\\",
+        \\"picocolors\\": \\"^1.0.0\\",
+        \\"source-map-js\\": \\"^1.0.2\\"
+      },
+      \\"engines\\": {
+        \\"node\\": \\"^10 || ^12 || >=14\\"
+      },
+      \\"funding\\": {
+        \\"type\\": \\"opencollective\\",
+        \\"url\\": \\"https://opencollective.com/postcss/\\"
+      }
+    },
+    \\"node_modules/source-map-js\\": {
+      \\"version\\": \\"1.0.2\\",
+      \\"resolved\\": \\"https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz\\",
+      \\"integrity\\": \\"sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==\\",
+      \\"engines\\": {
+        \\"node\\": \\">=0.10.0\\"
+      }
+    }
+  },
+  \\"dependencies\\": {
+    \\"nanoid\\": {
+      \\"version\\": \\"3.3.1\\",
+      \\"resolved\\": \\"https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz\\",
+      \\"integrity\\": \\"sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==\\"
+    },
+    \\"picocolors\\": {
+      \\"version\\": \\"1.0.0\\",
+      \\"resolved\\": \\"https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz\\",
+      \\"integrity\\": \\"sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==\\"
+    },
+    \\"postcss\\": {
+      \\"version\\": \\"8.4.8\\",
+      \\"resolved\\": \\"https://registry.npmjs.org/postcss/-/postcss-8.4.8.tgz\\",
+      \\"integrity\\": \\"sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ==\\",
+      \\"requires\\": {
+        \\"nanoid\\": \\"^3.3.1\\",
+        \\"picocolors\\": \\"^1.0.0\\",
+        \\"source-map-js\\": \\"^1.0.2\\"
+      }
+    },
+    \\"source-map-js\\": {
+      \\"version\\": \\"1.0.2\\",
+      \\"resolved\\": \\"https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz\\",
+      \\"integrity\\": \\"sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==\\"
+    }
+  }
+}"
+`;
+
+exports[`modules/manager/npm/post-update/npm performs lock file updates retaining the package.json counterparts 2`] = `
+Array [
+  Object {
+    "cmd": "npm install --package-lock-only --no-audit --ignore-scripts postcss@8.4.8",
+    "options": Object {
+      "cwd": "some-dir",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
 exports[`modules/manager/npm/post-update/npm performs npm-shrinkwrap.json updates (no package-lock.json) 1`] = `Array []`;
 
 exports[`modules/manager/npm/post-update/npm performs npm-shrinkwrap.json updates 1`] = `Array []`;
diff --git a/lib/modules/manager/npm/post-update/npm.spec.ts b/lib/modules/manager/npm/post-update/npm.spec.ts
index d7054aadfd..8cb1a14cd6 100644
--- a/lib/modules/manager/npm/post-update/npm.spec.ts
+++ b/lib/modules/manager/npm/post-update/npm.spec.ts
@@ -2,7 +2,7 @@ import { exec as _exec } from 'child_process';
 import upath from 'upath';
 
 import { envMock, mockExecAll } from '../../../../../test/exec-util';
-import { mocked } from '../../../../../test/util';
+import { loadFixture, mocked } from '../../../../../test/util';
 import * as _env from '../../../../util/exec/env';
 import * as _fs from '../../../../util/fs/proxies';
 import * as npmHelper from './npm';
@@ -61,6 +61,33 @@ describe('modules/manager/npm/post-update/npm', () => {
     expect(res.lockFile).toBe('package-lock-contents');
     expect(execSnapshots).toMatchSnapshot();
   });
+  it('performs lock file updates retaining the package.json counterparts', async () => {
+    const execSnapshots = mockExecAll(exec);
+    fs.readFile = jest.fn(() =>
+      loadFixture('update-lockfile-massage-1/package-lock.json')
+    ) as never;
+    const skipInstalls = true;
+    const updates = [
+      {
+        depName: 'postcss',
+        depType: 'dependencies',
+        newVersion: '8.4.8',
+        newValue: '^8.0.0',
+        isLockfileUpdate: true,
+      },
+    ];
+    const res = await npmHelper.generateLockFile(
+      'some-dir',
+      {},
+      'package-lock.json',
+      { skipInstalls },
+      updates
+    );
+    expect(fs.readFile).toHaveBeenCalledTimes(1);
+    expect(res.error).toBeUndefined();
+    expect(res.lockFile).toMatchSnapshot();
+    expect(execSnapshots).toMatchSnapshot();
+  });
   it('performs npm-shrinkwrap.json updates', async () => {
     const execSnapshots = mockExecAll(exec);
     fs.pathExists.mockResolvedValueOnce(true);
diff --git a/lib/modules/manager/npm/post-update/npm.ts b/lib/modules/manager/npm/post-update/npm.ts
index 840840e2c2..a274c32945 100644
--- a/lib/modules/manager/npm/post-update/npm.ts
+++ b/lib/modules/manager/npm/post-update/npm.ts
@@ -1,3 +1,4 @@
+import detectIndent from 'detect-indent';
 import upath from 'upath';
 import { GlobalConfig } from '../../../../config/global';
 import {
@@ -123,6 +124,33 @@ export async function generateLockFile(
 
     // Read the result
     lockFile = await readFile(upath.join(cwd, filename), 'utf8');
+
+    // Massage lockfile counterparts of package.json that were modified
+    // because npm install was called with an explicit version for rangeStrategy=update-lockfile
+    if (lockUpdates.length) {
+      let detectedIndent: string;
+      let lockFileParsed: any;
+      try {
+        detectedIndent = detectIndent(lockFile).indent || '  ';
+        lockFileParsed = JSON.parse(lockFile);
+      } catch (err) {
+        logger.warn({ err }, 'Error parsing npm lock file');
+      }
+      if (lockFileParsed?.lockfileVersion === 2) {
+        lockUpdates.forEach((lockUpdate) => {
+          if (
+            lockFileParsed.packages?.['']?.[lockUpdate.depType]?.[
+              lockUpdate.depName
+            ]
+          ) {
+            lockFileParsed.packages[''][lockUpdate.depType][
+              lockUpdate.depName
+            ] = lockUpdate.newValue;
+          }
+        });
+        lockFile = JSON.stringify(lockFileParsed, null, detectedIndent);
+      }
+    }
   } catch (err) /* istanbul ignore next */ {
     if (err.message === TEMPORARY_ERROR) {
       throw err;
-- 
GitLab