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