diff --git a/lib/modules/manager/npm/extract/index.spec.ts b/lib/modules/manager/npm/extract/index.spec.ts index 0b966aab70a2cb9b6e4317a58e3bacb8652c280f..6ed0df8fb39ed07fac130f6691f8dbf1705b51bf 100644 --- a/lib/modules/manager/npm/extract/index.spec.ts +++ b/lib/modules/manager/npm/extract/index.spec.ts @@ -1,9 +1,12 @@ import { Fixtures } from '../../../../../test/fixtures'; +import { fs } from '../../../../../test/util'; import { getConfig } from '../../../../config/defaults'; -import * as _fs from '../../../../util/fs'; import * as npmExtract from '.'; -const fs: any = _fs; +jest.mock('../../../../util/fs'); +const realFs = jest.requireActual<typeof import('../../../../util/fs')>( + '../../../../util/fs' +); // TODO: fix types const defaultConfig = getConfig(); @@ -22,8 +25,10 @@ const invalidNameContent = Fixtures.get('invalid-name.json', '..'); describe('modules/manager/npm/extract/index', () => { describe('.extractPackageFile()', () => { beforeEach(() => { - fs.readLocalFile = jest.fn(() => null); - fs.localPathExists = jest.fn(() => false); + jest.resetAllMocks(); + fs.readLocalFile.mockResolvedValue(null); + fs.localPathExists.mockResolvedValue(false); + fs.getSiblingFileName.mockImplementation(realFs.getSiblingFileName); }); it('returns null if cannot parse', async () => { @@ -140,11 +145,11 @@ describe('modules/manager/npm/extract/index', () => { }); it('finds a lock file', async () => { - fs.readLocalFile = jest.fn((fileName) => { + fs.readLocalFile.mockImplementation((fileName): Promise<any> => { if (fileName === 'yarn.lock') { - return '# yarn.lock'; + return Promise.resolve('# yarn.lock'); } - return null; + return Promise.resolve(null); }); const res = await npmExtract.extractPackageFile( input01Content, @@ -155,11 +160,11 @@ describe('modules/manager/npm/extract/index', () => { }); it('finds and filters .npmrc', async () => { - fs.readLocalFile = jest.fn((fileName) => { + fs.readLocalFile.mockImplementation((fileName): Promise<any> => { if (fileName === '.npmrc') { - return 'save-exact = true\npackage-lock = false\n'; + return Promise.resolve('save-exact = true\npackage-lock = false\n'); } - return null; + return Promise.resolve(null); }); const res = await npmExtract.extractPackageFile( input01Content, @@ -170,7 +175,7 @@ describe('modules/manager/npm/extract/index', () => { }); it('uses config.npmrc if no .npmrc exists', async () => { - fs.readLocalFile = jest.fn(() => null); + fs.readLocalFile.mockResolvedValueOnce(null); const res = await npmExtract.extractPackageFile( input01Content, 'package.json', @@ -180,11 +185,11 @@ describe('modules/manager/npm/extract/index', () => { }); it('uses config.npmrc if .npmrc does exist but npmrcMerge=false', async () => { - fs.readLocalFile = jest.fn((fileName) => { + fs.readLocalFile.mockImplementation((fileName): Promise<any> => { if (fileName === '.npmrc') { - return 'repo-npmrc\n'; + return Promise.resolve('repo-npmrc\n'); } - return null; + return Promise.resolve(null); }); const res = await npmExtract.extractPackageFile( input01Content, @@ -195,11 +200,11 @@ describe('modules/manager/npm/extract/index', () => { }); it('merges config.npmrc and repo .npmrc when npmrcMerge=true', async () => { - fs.readLocalFile = jest.fn((fileName) => { + fs.readLocalFile.mockImplementation((fileName): Promise<any> => { if (fileName === '.npmrc') { - return 'repo-npmrc\n'; + return Promise.resolve('repo-npmrc\n'); } - return null; + return Promise.resolve(null); }); const res = await npmExtract.extractPackageFile( input01Content, @@ -210,11 +215,13 @@ describe('modules/manager/npm/extract/index', () => { }); it('finds and filters .npmrc with variables', async () => { - fs.readLocalFile = jest.fn((fileName) => { + fs.readLocalFile.mockImplementation((fileName): Promise<any> => { if (fileName === '.npmrc') { - return 'registry=https://registry.npmjs.org\n//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}\n'; + return Promise.resolve( + 'registry=https://registry.npmjs.org\n//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}\n' + ); } - return null; + return Promise.resolve(null); }); const res = await npmExtract.extractPackageFile( input01Content, @@ -225,11 +232,30 @@ describe('modules/manager/npm/extract/index', () => { }); it('reads registryUrls from .yarnrc.yml', async () => { - fs.readLocalFile = jest.fn((fileName) => { + fs.readLocalFile.mockImplementation((fileName): Promise<any> => { if (fileName === '.yarnrc.yml') { - return 'npmRegistryServer: https://registry.example.com'; + return Promise.resolve( + 'npmRegistryServer: https://registry.example.com' + ); + } + return Promise.resolve(null); + }); + const res = await npmExtract.extractPackageFile( + input02Content, + 'package.json', + {} + ); + expect( + res?.deps.flatMap((dep) => dep.registryUrls) + ).toBeArrayIncludingOnly(['https://registry.example.com']); + }); + + it('reads registryUrls from .yarnrc', async () => { + fs.readLocalFile.mockImplementation((fileName): Promise<any> => { + if (fileName === '.yarnrc') { + return Promise.resolve('registry "https://registry.example.com"'); } - return null; + return Promise.resolve(null); }); const res = await npmExtract.extractPackageFile( input02Content, @@ -242,11 +268,11 @@ describe('modules/manager/npm/extract/index', () => { }); it('finds lerna', async () => { - fs.readLocalFile = jest.fn((fileName) => { + fs.readLocalFile.mockImplementation((fileName): Promise<any> => { if (fileName === 'lerna.json') { - return '{}'; + return Promise.resolve('{}'); } - return null; + return Promise.resolve(null); }); const res = await npmExtract.extractPackageFile( input01Content, @@ -261,11 +287,11 @@ describe('modules/manager/npm/extract/index', () => { }); it('finds "npmClient":"npm" in lerna.json', async () => { - fs.readLocalFile = jest.fn((fileName) => { + fs.readLocalFile.mockImplementation((fileName): Promise<any> => { if (fileName === 'lerna.json') { - return '{ "npmClient": "npm" }'; + return Promise.resolve('{ "npmClient": "npm" }'); } - return null; + return Promise.resolve(null); }); const res = await npmExtract.extractPackageFile( input01Content, @@ -280,11 +306,11 @@ describe('modules/manager/npm/extract/index', () => { }); it('finds "npmClient":"yarn" in lerna.json', async () => { - fs.readLocalFile = jest.fn((fileName) => { + fs.readLocalFile.mockImplementation((fileName): Promise<any> => { if (fileName === 'lerna.json') { - return '{ "npmClient": "yarn" }'; + return Promise.resolve('{ "npmClient": "yarn" }'); } - return null; + return Promise.resolve(null); }); const res = await npmExtract.extractPackageFile( input01Content, @@ -299,11 +325,11 @@ describe('modules/manager/npm/extract/index', () => { }); it('finds simple yarn workspaces', async () => { - fs.readLocalFile = jest.fn((fileName) => { + fs.readLocalFile.mockImplementation((fileName): Promise<any> => { if (fileName === 'lerna.json') { - return '{}'; + return Promise.resolve('{}'); } - return null; + return Promise.resolve(null); }); const res = await npmExtract.extractPackageFile( workspacesSimpleContent, @@ -314,11 +340,11 @@ describe('modules/manager/npm/extract/index', () => { }); it('finds simple yarn workspaces with lerna.json and useWorkspaces: true', async () => { - fs.readLocalFile = jest.fn((fileName) => { + fs.readLocalFile.mockImplementation((fileName): Promise<any> => { if (fileName === 'lerna.json') { - return '{"useWorkspaces": true}'; + return Promise.resolve('{"useWorkspaces": true}'); } - return null; + return Promise.resolve(null); }); const res = await npmExtract.extractPackageFile( workspacesSimpleContent, @@ -329,11 +355,11 @@ describe('modules/manager/npm/extract/index', () => { }); it('finds complex yarn workspaces', async () => { - fs.readLocalFile = jest.fn((fileName) => { + fs.readLocalFile.mockImplementation((fileName): Promise<any> => { if (fileName === 'lerna.json') { - return '{}'; + return Promise.resolve('{}'); } - return null; + return Promise.resolve(null); }); const res = await npmExtract.extractPackageFile( workspacesContent, @@ -676,11 +702,11 @@ describe('modules/manager/npm/extract/index', () => { }); it('extracts npm package alias', async () => { - fs.readLocalFile = jest.fn((fileName) => { + fs.readLocalFile.mockImplementation((fileName: string): Promise<any> => { if (fileName === 'package-lock.json') { - return '{}'; + return Promise.resolve('{}'); } - return null; + return Promise.resolve(null); }); const pJson = { dependencies: { @@ -705,16 +731,16 @@ describe('modules/manager/npm/extract/index', () => { }); it('sets skipInstalls false if Yarn zero-install is used', async () => { - fs.readLocalFile = jest.fn((fileName) => { + fs.readLocalFile.mockImplementation((fileName): Promise<any> => { if (fileName === 'yarn.lock') { - return '# yarn.lock'; + return Promise.resolve('# yarn.lock'); } if (fileName === '.yarnrc.yml') { - return 'pnpEnableInlining: false'; + return Promise.resolve('pnpEnableInlining: false'); } - return null; + return Promise.resolve(null); }); - fs.localPathExists = jest.fn(() => true); + fs.localPathExists.mockResolvedValueOnce(true); const res = await npmExtract.extractPackageFile( input01Content, 'package.json', diff --git a/lib/modules/manager/npm/extract/index.ts b/lib/modules/manager/npm/extract/index.ts index a4425de015b5efff5e3ee30eb3351c2d2eeafeb2..7bdb16c5283bbd7274832319d340d90514ea54c5 100644 --- a/lib/modules/manager/npm/extract/index.ts +++ b/lib/modules/manager/npm/extract/index.ts @@ -22,6 +22,7 @@ import type { NpmPackage, NpmPackageDependency } from './types'; import { isZeroInstall } from './yarn'; import { YarnConfig, + loadConfigFromLegacyYarnrc, loadConfigFromYarnrcYml, resolveRegistryUrl, } from './yarnrc'; @@ -149,6 +150,12 @@ export async function extractPackageFile( yarnConfig = loadConfigFromYarnrcYml(repoYarnrcYml); } + const legacyYarnrcFileName = getSiblingFileName(fileName, '.yarnrc'); + const repoLegacyYarnrc = await readLocalFile(legacyYarnrcFileName, 'utf8'); + if (is.string(repoLegacyYarnrc)) { + yarnConfig = loadConfigFromLegacyYarnrc(repoLegacyYarnrc); + } + let lernaJsonFile: string | undefined; let lernaPackages: string[] | undefined; let lernaClient: 'yarn' | 'npm' | undefined; diff --git a/lib/modules/manager/npm/extract/yarnrc.spec.ts b/lib/modules/manager/npm/extract/yarnrc.spec.ts index 0b77d7ccf34cf9ebe314e536292901c315b2e30a..be41b1593cbd547b50fa92104bc10e7f81413c39 100644 --- a/lib/modules/manager/npm/extract/yarnrc.spec.ts +++ b/lib/modules/manager/npm/extract/yarnrc.spec.ts @@ -1,5 +1,9 @@ import { codeBlock } from 'common-tags'; -import { loadConfigFromYarnrcYml, resolveRegistryUrl } from './yarnrc'; +import { + loadConfigFromLegacyYarnrc, + loadConfigFromYarnrcYml, + resolveRegistryUrl, +} from './yarnrc'; describe('modules/manager/npm/extract/yarnrc', () => { describe('resolveRegistryUrl()', () => { @@ -108,4 +112,51 @@ describe('modules/manager/npm/extract/yarnrc', () => { expect(config).toEqual(expectedConfig); }); }); + + describe('loadConfigFromLegacyYarnrc()', () => { + it.each([ + [ + codeBlock` + # yarn lockfile v1 + registry "https://npm.example.com" + `, + { + npmRegistryServer: 'https://npm.example.com', + }, + ], + [ + codeBlock` + disturl "https://npm-dist.example.com" + registry https://npm.example.com + sass_binary_site "https://node-sass.example.com" + `, + { + npmRegistryServer: 'https://npm.example.com', + }, + ], + [ + codeBlock` + --install.frozen-lockfile true + "registry" "https://npm.example.com" + "@foo:registry" "https://npm-foo.example.com" + "@bar:registry" "https://npm-bar.example.com" + `, + { + npmRegistryServer: 'https://npm.example.com', + npmScopes: { + foo: { + npmRegistryServer: 'https://npm-foo.example.com', + }, + bar: { + npmRegistryServer: 'https://npm-bar.example.com', + }, + }, + }, + ], + ])('produces expected config (%s)', (legacyYarnrc, expectedConfig) => { + const config = loadConfigFromLegacyYarnrc(legacyYarnrc); + + expect(config).toEqual(expectedConfig); + }); + }); }); diff --git a/lib/modules/manager/npm/extract/yarnrc.ts b/lib/modules/manager/npm/extract/yarnrc.ts index 49e32e218436a4a59e317ee8dccf1704ea818347..06e5d9caf0141f9a54e84f372796f17a67f1235f 100644 --- a/lib/modules/manager/npm/extract/yarnrc.ts +++ b/lib/modules/manager/npm/extract/yarnrc.ts @@ -1,6 +1,8 @@ +import is from '@sindresorhus/is'; import { load } from 'js-yaml'; import { z } from 'zod'; import { logger } from '../../../../logger'; +import { regEx } from '../../../../util/regex'; const YarnrcYmlSchema = z.object({ npmRegistryServer: z.string().optional(), @@ -15,6 +17,31 @@ const YarnrcYmlSchema = z.object({ export type YarnConfig = z.infer<typeof YarnrcYmlSchema>; +const registryRegEx = regEx( + /^"?(@(?<scope>[^:]+):)?registry"? "?(?<registryUrl>[^"]+)"?$/gm +); + +export function loadConfigFromLegacyYarnrc( + legacyYarnrc: string +): YarnConfig | null { + const registryMatches = [...legacyYarnrc.matchAll(registryRegEx)] + .map((m) => m.groups) + .filter(is.truthy); + + const yarnConfig: YarnConfig = {}; + for (const registryMatch of registryMatches) { + if (registryMatch.scope) { + yarnConfig.npmScopes ??= {}; + yarnConfig.npmScopes[registryMatch.scope] ??= {}; + yarnConfig.npmScopes[registryMatch.scope].npmRegistryServer = + registryMatch.registryUrl; + } else { + yarnConfig.npmRegistryServer = registryMatch.registryUrl; + } + } + return yarnConfig; +} + export function loadConfigFromYarnrcYml(yarnrcYml: string): YarnConfig | null { try { const obj = load(yarnrcYml, {