From 0f226139ccada7d92ef03cf904501b9d28d2fae9 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Thu, 23 Sep 2021 13:17:44 +0200
Subject: [PATCH] feat(npm): npmrcMerge (#11857)

Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>
---
 docs/usage/configuration-options.md           |  9 ++++++++
 .../usage/getting-started/private-packages.md |  2 ++
 lib/config/options/index.ts                   |  8 +++++++
 lib/config/types.ts                           |  1 +
 lib/manager/npm/extract/index.spec.ts         | 16 +++++++++++++-
 lib/manager/npm/extract/index.ts              | 22 +++++++++++--------
 lib/manager/types.ts                          |  1 +
 7 files changed, 49 insertions(+), 10 deletions(-)

diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index 10f6c161de..3d4b58ed79 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -1232,6 +1232,15 @@ Typically you would encrypt it and put it inside the `encrypted` object.
 
 See [Private npm module support](https://docs.renovatebot.com/getting-started/private-packages) for details on how this is used.
 
+## npmrcMerge
+
+This option exists to provide flexibility about whether `npmrc` strings in config should override `.npmrc` files in the repo, or be merged with them.
+In some situations you need the ability to force override `.npmrc` contents in a repo (`npmMerge=false`) while in others you might want to simply supplement the settings already in the `.npmrc` (`npmMerge=true`).
+A use case for the latter is if you are a Renovate bot admin and wish to provide a default token for `npmjs.org` without removing any other `.npmrc` settings which individual repositories have configured (such as scopes/registries).
+
+If `false` (default), it means that defining `config.npmrc` will result in any `.npmrc` file in the repo being overridden and therefore its values ignored.
+If configured to `true`, it means that any `.npmrc` file in the repo will have `config.npmrc` prepended to it before running `npm`.
+
 ## packageRules
 
 `packageRules` is a powerful feature that lets you apply rules to individual packages or to groups of packages using regex pattern matching.
diff --git a/docs/usage/getting-started/private-packages.md b/docs/usage/getting-started/private-packages.md
index 9007d04028..0af1ac3113 100644
--- a/docs/usage/getting-started/private-packages.md
+++ b/docs/usage/getting-started/private-packages.md
@@ -180,6 +180,8 @@ You can add an `.npmrc` authentication line to your Renovate config under the fi
 ```
 
 If configured like this, Renovate will use this to authenticate with npm and will ignore any `.npmrc` files(s) it finds checked into the repository.
+If you wish for the values in your `config.npmrc` to be _merged_ (prepended) with any values found in repos then also set `config.npmrcMerge=true`.
+This merge approach is similar to how `npm` itself behaves if `.npmrc` is found in both the user home directory as well as a project.
 
 #### Add npmToken to Renovate config
 
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index 7a757b04d8..90105c4e4d 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -589,6 +589,14 @@ const options: RenovateOptions[] = [
     stage: 'branch',
     type: 'string',
   },
+  {
+    name: 'npmrcMerge',
+    description:
+      'Whether to merge config.npmrc with repo .npmrc content if both are found.',
+    stage: 'branch',
+    type: 'boolean',
+    default: false,
+  },
   {
     name: 'npmToken',
     description: 'npm token used for authenticating with the default registry.',
diff --git a/lib/config/types.ts b/lib/config/types.ts
index 6094917fa2..a773d15247 100644
--- a/lib/config/types.ts
+++ b/lib/config/types.ts
@@ -44,6 +44,7 @@ export interface RenovateSharedConfig {
   dependencyDashboardApproval?: boolean;
   hashedBranchLength?: number;
   npmrc?: string;
+  npmrcMerge?: boolean;
   platform?: string;
   postUpgradeTasks?: PostUpgradeTasks;
   prBodyColumns?: string[];
diff --git a/lib/manager/npm/extract/index.spec.ts b/lib/manager/npm/extract/index.spec.ts
index e513d19e92..538c8fbea6 100644
--- a/lib/manager/npm/extract/index.spec.ts
+++ b/lib/manager/npm/extract/index.spec.ts
@@ -122,7 +122,7 @@ describe('manager/npm/extract/index', () => {
       );
       expect(res.npmrc).toBeDefined();
     });
-    it('ignores .npmrc when config.npmrc is defined', async () => {
+    it('ignores .npmrc when config.npmrc is defined and npmrcMerge=false', async () => {
       fs.readLocalFile = jest.fn((fileName) => {
         if (fileName === '.npmrc') {
           return 'some-npmrc\n';
@@ -136,6 +136,20 @@ describe('manager/npm/extract/index', () => {
       );
       expect(res.npmrc).toBeUndefined();
     });
+    it('reads .npmrc when config.npmrc is merged', async () => {
+      fs.readLocalFile = jest.fn((fileName) => {
+        if (fileName === '.npmrc') {
+          return 'repo-npmrc\n';
+        }
+        return null;
+      });
+      const res = await npmExtract.extractPackageFile(
+        input01Content,
+        'package.json',
+        { npmrc: 'config-npmrc', npmrcMerge: true }
+      );
+      expect(res.npmrc).toEqual(`config-npmrc\nrepo-npmrc\n`);
+    });
     it('finds and filters .npmrc with variables', async () => {
       fs.readLocalFile = jest.fn((fileName) => {
         if (fileName === '.npmrc') {
diff --git a/lib/manager/npm/extract/index.ts b/lib/manager/npm/extract/index.ts
index 4cf3967905..e3a7bf30a4 100644
--- a/lib/manager/npm/extract/index.ts
+++ b/lib/manager/npm/extract/index.ts
@@ -96,29 +96,33 @@ export async function extractPackageFile(
 
   let npmrc: string;
   const npmrcFileName = getSiblingFileName(fileName, '.npmrc');
-  const npmrcContent = await readLocalFile(npmrcFileName, 'utf8');
-  if (is.string(npmrcContent)) {
-    if (is.string(config.npmrc)) {
+  let repoNpmrc = await readLocalFile(npmrcFileName, 'utf8');
+  if (is.string(repoNpmrc)) {
+    if (is.string(config.npmrc) && !config.npmrcMerge) {
       logger.debug(
         { npmrcFileName },
-        'Repo .npmrc file is ignored due to presence of config.npmrc'
+        'Repo .npmrc file is ignored due to config.npmrc with config.npmrcMerge=force'
       );
     } else {
-      npmrc = npmrcContent;
-      if (npmrc?.includes('package-lock')) {
+      npmrc = config.npmrc || '';
+      if (npmrc.length) {
+        npmrc = npmrc.replace(/\n?$/, '\n');
+      }
+      if (repoNpmrc?.includes('package-lock')) {
         logger.debug('Stripping package-lock setting from .npmrc');
-        npmrc = npmrc.replace(/(^|\n)package-lock.*?(\n|$)/g, '\n');
+        repoNpmrc = repoNpmrc.replace(/(^|\n)package-lock.*?(\n|$)/g, '\n');
       }
-      if (npmrc.includes('=${') && !getGlobalConfig().exposeAllEnv) {
+      if (repoNpmrc.includes('=${') && !getGlobalConfig().exposeAllEnv) {
         logger.debug(
           { npmrcFileName },
           'Stripping .npmrc file of lines with variables'
         );
-        npmrc = npmrc
+        repoNpmrc = repoNpmrc
           .split('\n')
           .filter((line) => !line.includes('=${'))
           .join('\n');
       }
+      npmrc += repoNpmrc;
     }
   }
 
diff --git a/lib/manager/types.ts b/lib/manager/types.ts
index 69358a103e..5e1842c53d 100644
--- a/lib/manager/types.ts
+++ b/lib/manager/types.ts
@@ -20,6 +20,7 @@ export interface ExtractConfig {
   gradle?: { timeout?: number };
   aliases?: Record<string, string>;
   npmrc?: string;
+  npmrcMerge?: boolean;
   skipInstalls?: boolean;
   updateInternalDeps?: boolean;
   deepExtract?: boolean;
-- 
GitLab