From f9ce0e1004d854bd32eecd52a7a374a114bc34a7 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Sun, 27 Feb 2022 11:36:45 +0100
Subject: [PATCH] fix(npm): extract packageRules from npmrc (#14433)

---
 lib/config/presets/npm/index.spec.ts |   1 -
 lib/datasource/npm/common.ts         |   1 +
 lib/datasource/npm/index.spec.ts     |   5 +-
 lib/datasource/npm/index.ts          |   2 +-
 lib/datasource/npm/npmrc.spec.ts     |  27 ++++-
 lib/datasource/npm/npmrc.ts          |  66 +++++++++---
 lib/datasource/npm/types.ts          |   2 +
 package.json                         |   2 -
 yarn.lock                            | 150 +++++++++++++--------------
 9 files changed, 157 insertions(+), 99 deletions(-)

diff --git a/lib/config/presets/npm/index.spec.ts b/lib/config/presets/npm/index.spec.ts
index 3c55df68bb..3cb43fcfc8 100644
--- a/lib/config/presets/npm/index.spec.ts
+++ b/lib/config/presets/npm/index.spec.ts
@@ -2,7 +2,6 @@ import * as httpMock from '../../../../test/http-mock';
 import { GlobalConfig } from '../../global';
 import * as npm from '.';
 
-jest.mock('registry-auth-token');
 jest.mock('delay');
 
 describe('config/presets/npm/index', () => {
diff --git a/lib/datasource/npm/common.ts b/lib/datasource/npm/common.ts
index 1a8364805b..0307444a0c 100644
--- a/lib/datasource/npm/common.ts
+++ b/lib/datasource/npm/common.ts
@@ -1 +1,2 @@
+export const defaultRegistryUrls = ['https://registry.npmjs.org'];
 export const id = 'npm';
diff --git a/lib/datasource/npm/index.spec.ts b/lib/datasource/npm/index.spec.ts
index ec457904a4..92f25a64f7 100644
--- a/lib/datasource/npm/index.spec.ts
+++ b/lib/datasource/npm/index.spec.ts
@@ -4,7 +4,7 @@ import * as httpMock from '../../../test/http-mock';
 import { GlobalConfig } from '../../config/global';
 import { EXTERNAL_HOST_ERROR } from '../../constants/error-messages';
 import * as hostRules from '../../util/host-rules';
-import { NpmDatasource, getNpmrc, resetCache, setNpmrc } from '.';
+import { NpmDatasource, resetCache, setNpmrc } from '.';
 
 const datasource = NpmDatasource.id;
 
@@ -305,8 +305,7 @@ describe('datasource/npm/index', () => {
     const npmrcContent = 'something=something';
     setNpmrc(npmrcContent);
     setNpmrc(npmrcContent);
-    setNpmrc();
-    expect(getNpmrc()).toBeEmptyObject();
+    expect(setNpmrc()).toBeUndefined();
   });
 
   it('should use default registry if missing from npmrc', async () => {
diff --git a/lib/datasource/npm/index.ts b/lib/datasource/npm/index.ts
index a3c19643d2..b66c17597c 100644
--- a/lib/datasource/npm/index.ts
+++ b/lib/datasource/npm/index.ts
@@ -7,7 +7,7 @@ import { getDependency } from './get';
 import { setNpmrc } from './npmrc';
 
 export { resetMemCache, resetCache } from './get';
-export { getNpmrc, setNpmrc } from './npmrc';
+export { setNpmrc } from './npmrc';
 
 export const customRegistrySupport = false;
 
diff --git a/lib/datasource/npm/npmrc.spec.ts b/lib/datasource/npm/npmrc.spec.ts
index 78f337551b..1515d1c90b 100644
--- a/lib/datasource/npm/npmrc.spec.ts
+++ b/lib/datasource/npm/npmrc.spec.ts
@@ -5,7 +5,6 @@ import * as _sanitize from '../../util/sanitize';
 import {
   convertNpmrcToRules,
   getMatchHostFromNpmrcHost,
-  getNpmrc,
   setNpmrc,
 } from './npmrc';
 
@@ -37,6 +36,13 @@ describe('datasource/npm/npmrc', () => {
     });
   });
   describe('convertNpmrcToRules()', () => {
+    it('rejects invalid registries', () => {
+      const res = convertNpmrcToRules(
+        ini.parse('registry=1\n@scope:registry=2\n')
+      );
+      expect(res.hostRules).toHaveLength(0);
+      expect(res.packageRules).toHaveLength(0);
+    });
     it('handles naked auth', () => {
       expect(convertNpmrcToRules(ini.parse('_auth=abc123\n')))
         .toMatchInlineSnapshot(`
@@ -48,6 +54,7 @@ describe('datasource/npm/npmrc', () => {
               "token": "abc123",
             },
           ],
+          "packageRules": Array [],
         }
       `);
     });
@@ -64,6 +71,7 @@ describe('datasource/npm/npmrc', () => {
               "token": "abc123",
             },
           ],
+          "packageRules": Array [],
         }
       `);
     });
@@ -81,6 +89,7 @@ describe('datasource/npm/npmrc', () => {
               "token": "abc123",
             },
           ],
+          "packageRules": Array [],
         }
       `);
     });
@@ -94,6 +103,7 @@ describe('datasource/npm/npmrc', () => {
               "token": "abc123",
             },
           ],
+          "packageRules": Array [],
         }
       `);
     });
@@ -113,6 +123,19 @@ describe('datasource/npm/npmrc', () => {
               "token": "abc123",
             },
           ],
+          "packageRules": Array [
+            Object {
+              "matchDataSources": Array [
+                "npm",
+              ],
+              "matchPackagePrefixes": Array [
+                "@fontawesome/",
+              ],
+              "registryUrls": Array [
+                "https://npm.fontawesome.com/",
+              ],
+            },
+          ],
         }
       `);
     });
@@ -133,6 +156,7 @@ describe('datasource/npm/npmrc', () => {
               "username": "bot",
             },
           ],
+          "packageRules": Array [],
         }
       `);
     });
@@ -174,6 +198,5 @@ describe('datasource/npm/npmrc', () => {
   it('ignores localhost', () => {
     setNpmrc(`registry=http://localhost`);
     expect(sanitize.addSecretForSanitizing).toHaveBeenCalledTimes(0);
-    expect(getNpmrc()).toBeEmptyObject();
   });
 });
diff --git a/lib/datasource/npm/npmrc.ts b/lib/datasource/npm/npmrc.ts
index 712ceb8735..33d3841b38 100644
--- a/lib/datasource/npm/npmrc.ts
+++ b/lib/datasource/npm/npmrc.ts
@@ -1,21 +1,20 @@
 import url from 'url';
 import is from '@sindresorhus/is';
 import ini from 'ini';
-import getRegistryUrl from 'registry-auth-token/registry-url.js';
 import { GlobalConfig } from '../../config/global';
+import type { PackageRule } from '../../config/types';
 import { logger } from '../../logger';
 import type { HostRule } from '../../types';
 import * as hostRules from '../../util/host-rules';
 import { regEx } from '../../util/regex';
 import { fromBase64 } from '../../util/string';
-import type { Npmrc, NpmrcRules, PackageResolution } from './types';
+import { ensureTrailingSlash, validateUrl } from '../../util/url';
+import { defaultRegistryUrls } from './common';
+import type { NpmrcRules, PackageResolution } from './types';
 
 let npmrc: Record<string, any> = {};
 let npmrcRaw = '';
-
-export function getNpmrc(): Npmrc | null {
-  return npmrc;
-}
+let packageRules: PackageRule[] = [];
 
 function envReplace(value: any, env = process.env): any {
   // istanbul ignore if
@@ -48,7 +47,9 @@ export function getMatchHostFromNpmrcHost(input: string): string {
 export function convertNpmrcToRules(npmrc: Record<string, any>): NpmrcRules {
   const rules: NpmrcRules = {
     hostRules: [],
+    packageRules: [],
   };
+  // Generate hostRules
   const hostType = 'npm';
   const hosts: Record<string, HostRule> = {};
   for (const [key, value] of Object.entries(npmrc)) {
@@ -83,6 +84,41 @@ export function convertNpmrcToRules(npmrc: Record<string, any>): NpmrcRules {
     }
     rules.hostRules.push(hostRule);
   }
+  // Generate packageRules
+  const matchDataSources = ['npm'];
+  const { registry } = npmrc;
+  // packageRules order matters, so look for a default registry first
+  if (is.nonEmptyString(registry)) {
+    if (validateUrl(registry)) {
+      // Default registry
+      rules.packageRules.push({
+        matchDataSources,
+        registryUrls: [registry],
+      });
+    } else {
+      logger.warn({ registry }, 'Invalid npmrc registry= URL');
+    }
+  }
+  // Now look for scoped registries
+  for (const [key, value] of Object.entries(npmrc)) {
+    if (!is.nonEmptyString(value)) {
+      continue;
+    }
+    const keyParts = key.split(':');
+    const keyType = keyParts.pop();
+    if (keyType === 'registry' && keyParts.length && is.nonEmptyString(value)) {
+      const scope = keyParts.join(':');
+      if (validateUrl(value)) {
+        rules.packageRules.push({
+          matchDataSources,
+          matchPackagePrefixes: [scope + '/'],
+          registryUrls: [value],
+        });
+      } else {
+        logger.warn({ scope, registry: value }, 'Invalid npmrc registry= URL');
+      }
+    }
+  }
   return rules;
 }
 
@@ -120,21 +156,27 @@ export function setNpmrc(input?: string): void {
     if (npmrcRules.hostRules.length) {
       npmrcRules.hostRules.forEach((hostRule) => hostRules.add(hostRule));
     }
+    packageRules = npmrcRules.packageRules;
   } else if (npmrc) {
     logger.debug('Resetting npmrc');
     npmrc = {};
     npmrcRaw = '';
+    packageRules = [];
   }
 }
 
 export function resolvePackage(packageName: string): PackageResolution {
-  const scope = packageName.split('/')[0];
-  let registryUrl: string;
-  try {
-    registryUrl = getRegistryUrl(scope, getNpmrc());
-  } catch (err) {
-    registryUrl = 'https://registry.npmjs.org/';
+  let registryUrl = defaultRegistryUrls[0];
+  for (const rule of packageRules) {
+    const { matchPackagePrefixes, registryUrls } = rule;
+    if (
+      !matchPackagePrefixes ||
+      packageName.startsWith(matchPackagePrefixes[0])
+    ) {
+      registryUrl = registryUrls[0];
+    }
   }
+  registryUrl = ensureTrailingSlash(registryUrl);
   const packageUrl = url.resolve(
     registryUrl,
     encodeURIComponent(packageName).replace(regEx(/^%40/), '@')
diff --git a/lib/datasource/npm/types.ts b/lib/datasource/npm/types.ts
index f6153bcfa0..787c01c7ff 100644
--- a/lib/datasource/npm/types.ts
+++ b/lib/datasource/npm/types.ts
@@ -1,8 +1,10 @@
+import type { PackageRule } from '../../config/types';
 import type { HostRule } from '../../types';
 import type { Release, ReleaseResult } from '../types';
 
 export interface NpmrcRules {
   hostRules?: HostRule[];
+  packageRules?: PackageRule[];
 }
 
 export interface NpmResponse {
diff --git a/package.json b/package.json
index 6f92898d93..499cb49e3b 100644
--- a/package.json
+++ b/package.json
@@ -189,7 +189,6 @@
     "p-queue": "6.6.2",
     "parse-link-header": "2.0.0",
     "redis": "4.0.3",
-    "registry-auth-token": "4.2.1",
     "remark": "13.0.0",
     "remark-github": "10.1.0",
     "semver": "7.3.5",
@@ -242,7 +241,6 @@
     "@types/nock": "10.0.3",
     "@types/node": "16.11.25",
     "@types/parse-link-header": "1.0.1",
-    "@types/registry-auth-token": "4.2.1",
     "@types/semver": "7.3.9",
     "@types/semver-stable": "3.0.0",
     "@types/semver-utils": "1.1.1",
diff --git a/yarn.lock b/yarn.lock
index 97c1099951..19520d3f9c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1942,7 +1942,6 @@
 
 "@renovate/eslint-plugin@https://github.com/renovatebot/eslint-plugin#v0.0.4":
   version "0.0.4"
-  uid "0c444386e79d6145901212507521b8a0a48af000"
   resolved "https://github.com/renovatebot/eslint-plugin#0c444386e79d6145901212507521b8a0a48af000"
 
 "@renovatebot/pep440@2.1.0":
@@ -2502,11 +2501,6 @@
   resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.4.tgz#5d9b63132df54d8909fce1c3f8ca260fdd693e17"
   integrity sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA==
 
-"@types/registry-auth-token@4.2.1":
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/@types/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6e83d9353bdc2c7183eb9e86fd0bac5f33d3c368"
-  integrity sha512-VtTUcUaJGiJtlBKYwwFIOSvrcnuKmpPGO+x56XijNZnaDpnzKh2VwoTw5hewrOMW2BgjoU+uFbVAvSCW2FpWmA==
-
 "@types/responselike@*", "@types/responselike@^1.0.0":
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29"
@@ -7283,77 +7277,77 @@ npm@^8.3.0:
   resolved "https://registry.yarnpkg.com/npm/-/npm-8.5.1.tgz#055960d856187d340a3af4d585930c7af92b568a"
   integrity sha512-zHrOHAatEPJ59o2JIPlhgc9LX9mb8xFrqu4kiiul4w1IGMTtKn2lqRiGIRKU0or69NSLXNmqbCP9bNJIr/wB6Q==
   dependencies:
-    "@isaacs/string-locale-compare" "*"
-    "@npmcli/arborist" "*"
-    "@npmcli/ci-detect" "*"
-    "@npmcli/config" "*"
-    "@npmcli/map-workspaces" "*"
-    "@npmcli/package-json" "*"
-    "@npmcli/run-script" "*"
-    abbrev "*"
-    ansicolors "*"
-    ansistyles "*"
-    archy "*"
-    cacache "*"
-    chalk "*"
-    chownr "*"
-    cli-columns "*"
-    cli-table3 "*"
-    columnify "*"
-    fastest-levenshtein "*"
-    glob "*"
-    graceful-fs "*"
-    hosted-git-info "*"
-    ini "*"
-    init-package-json "*"
-    is-cidr "*"
-    json-parse-even-better-errors "*"
-    libnpmaccess "*"
-    libnpmdiff "*"
-    libnpmexec "*"
-    libnpmfund "*"
-    libnpmhook "*"
-    libnpmorg "*"
-    libnpmpack "*"
-    libnpmpublish "*"
-    libnpmsearch "*"
-    libnpmteam "*"
-    libnpmversion "*"
-    make-fetch-happen "*"
-    minipass "*"
-    minipass-pipeline "*"
-    mkdirp "*"
-    mkdirp-infer-owner "*"
-    ms "*"
-    node-gyp "*"
-    nopt "*"
-    npm-audit-report "*"
-    npm-install-checks "*"
-    npm-package-arg "*"
-    npm-pick-manifest "*"
-    npm-profile "*"
-    npm-registry-fetch "*"
-    npm-user-validate "*"
-    npmlog "*"
-    opener "*"
-    pacote "*"
-    parse-conflict-json "*"
-    proc-log "*"
-    qrcode-terminal "*"
-    read "*"
-    read-package-json "*"
-    read-package-json-fast "*"
-    readdir-scoped-modules "*"
-    rimraf "*"
-    semver "*"
-    ssri "*"
-    tar "*"
-    text-table "*"
-    tiny-relative-date "*"
-    treeverse "*"
-    validate-npm-package-name "*"
-    which "*"
-    write-file-atomic "*"
+    "@isaacs/string-locale-compare" "^1.1.0"
+    "@npmcli/arborist" "^4.3.1"
+    "@npmcli/ci-detect" "^2.0.0"
+    "@npmcli/config" "^3.0.0"
+    "@npmcli/map-workspaces" "^2.0.0"
+    "@npmcli/package-json" "^1.0.1"
+    "@npmcli/run-script" "^2.0.0"
+    abbrev "~1.1.1"
+    ansicolors "~0.3.2"
+    ansistyles "~0.1.3"
+    archy "~1.0.0"
+    cacache "^15.3.0"
+    chalk "^4.1.2"
+    chownr "^2.0.0"
+    cli-columns "^4.0.0"
+    cli-table3 "^0.6.1"
+    columnify "~1.5.4"
+    fastest-levenshtein "^1.0.12"
+    glob "^7.2.0"
+    graceful-fs "^4.2.9"
+    hosted-git-info "^4.1.0"
+    ini "^2.0.0"
+    init-package-json "^2.0.5"
+    is-cidr "^4.0.2"
+    json-parse-even-better-errors "^2.3.1"
+    libnpmaccess "^5.0.1"
+    libnpmdiff "^3.0.0"
+    libnpmexec "^3.0.3"
+    libnpmfund "^2.0.2"
+    libnpmhook "^7.0.1"
+    libnpmorg "^3.0.1"
+    libnpmpack "^3.1.0"
+    libnpmpublish "^5.0.1"
+    libnpmsearch "^4.0.1"
+    libnpmteam "^3.0.1"
+    libnpmversion "^2.0.2"
+    make-fetch-happen "^10.0.3"
+    minipass "^3.1.6"
+    minipass-pipeline "^1.2.4"
+    mkdirp "^1.0.4"
+    mkdirp-infer-owner "^2.0.0"
+    ms "^2.1.2"
+    node-gyp "^8.4.1"
+    nopt "^5.0.0"
+    npm-audit-report "^2.1.5"
+    npm-install-checks "^4.0.0"
+    npm-package-arg "^8.1.5"
+    npm-pick-manifest "^6.1.1"
+    npm-profile "^6.0.0"
+    npm-registry-fetch "^12.0.2"
+    npm-user-validate "^1.0.1"
+    npmlog "^6.0.1"
+    opener "^1.5.2"
+    pacote "^12.0.3"
+    parse-conflict-json "^2.0.1"
+    proc-log "^1.0.0"
+    qrcode-terminal "^0.12.0"
+    read "~1.0.7"
+    read-package-json "^4.1.1"
+    read-package-json-fast "^2.0.3"
+    readdir-scoped-modules "^1.1.0"
+    rimraf "^3.0.2"
+    semver "^7.3.5"
+    ssri "^8.0.1"
+    tar "^6.1.11"
+    text-table "~0.2.0"
+    tiny-relative-date "^1.3.0"
+    treeverse "^1.0.4"
+    validate-npm-package-name "~3.0.0"
+    which "^2.0.2"
+    write-file-atomic "^4.0.0"
 
 npmlog@*, npmlog@^6.0.0:
   version "6.0.1"
@@ -8187,7 +8181,7 @@ regexpp@^3.2.0:
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
   integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
 
-registry-auth-token@4.2.1, registry-auth-token@^4.0.0:
+registry-auth-token@^4.0.0:
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250"
   integrity sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==
-- 
GitLab