diff --git a/lib/modules/manager/npm/__fixtures__/inputs/02.json b/lib/modules/manager/npm/__fixtures__/inputs/02.json index 6c2d055b3b0eafa15cd7cc34612a65ace03c41fa..d17132e5db5831ba0a338d9db0e4c7c538017a48 100644 --- a/lib/modules/manager/npm/__fixtures__/inputs/02.json +++ b/lib/modules/manager/npm/__fixtures__/inputs/02.json @@ -10,11 +10,7 @@ } ], "dependencies": { - "autoprefixer": "6.5.0", - "bower": "~1.6.0", - "browserify": "13.1.0", - "browserify-css": "0.9.2", - "cheerio": "0.22.0", + "@babel/core": "7.0.0", "config": "1.21.0" }, "homepage": "https://keylocation.sg", diff --git a/lib/modules/manager/npm/extract/__snapshots__/index.spec.ts.snap b/lib/modules/manager/npm/extract/__snapshots__/index.spec.ts.snap index 9962ebc67ae6580b3b84834c2b2db9c6dfb940c0..149a2913545481bd4663f81efd5f4eae281f4c85 100644 --- a/lib/modules/manager/npm/extract/__snapshots__/index.spec.ts.snap +++ b/lib/modules/manager/npm/extract/__snapshots__/index.spec.ts.snap @@ -1150,6 +1150,7 @@ exports[`modules/manager/npm/extract/index .extractPackageFile() finds simple ya } `; + exports[`modules/manager/npm/extract/index .extractPackageFile() returns an array of dependencies 1`] = ` { "constraints": {}, diff --git a/lib/modules/manager/npm/extract/index.spec.ts b/lib/modules/manager/npm/extract/index.spec.ts index 0529099f6f027e3021ebeaeb3a5fa3e81b687801..0b966aab70a2cb9b6e4317a58e3bacb8652c280f 100644 --- a/lib/modules/manager/npm/extract/index.spec.ts +++ b/lib/modules/manager/npm/extract/index.spec.ts @@ -9,6 +9,7 @@ const fs: any = _fs; const defaultConfig = getConfig(); const input01Content = Fixtures.get('inputs/01.json', '..'); +const input02Content = Fixtures.get('inputs/02.json', '..'); const input01GlobContent = Fixtures.get('inputs/01-glob.json', '..'); const workspacesContent = Fixtures.get('inputs/workspaces.json', '..'); const workspacesSimpleContent = Fixtures.get( @@ -223,6 +224,23 @@ describe('modules/manager/npm/extract/index', () => { expect(res?.npmrc).toBe('registry=https://registry.npmjs.org\n'); }); + it('reads registryUrls from .yarnrc.yml', async () => { + fs.readLocalFile = jest.fn((fileName) => { + if (fileName === '.yarnrc.yml') { + return 'npmRegistryServer: https://registry.example.com'; + } + return null; + }); + const res = await npmExtract.extractPackageFile( + input02Content, + 'package.json', + {} + ); + expect( + res?.deps.flatMap((dep) => dep.registryUrls) + ).toBeArrayIncludingOnly(['https://registry.example.com']); + }); + it('finds lerna', async () => { fs.readLocalFile = jest.fn((fileName) => { if (fileName === 'lerna.json') { diff --git a/lib/modules/manager/npm/extract/index.ts b/lib/modules/manager/npm/extract/index.ts index a69b98eac1573eb6df1a66e4ab4d29d0a4a84a5a..a4425de015b5efff5e3ee30eb3351c2d2eeafeb2 100644 --- a/lib/modules/manager/npm/extract/index.ts +++ b/lib/modules/manager/npm/extract/index.ts @@ -20,6 +20,11 @@ import { getLockedVersions } from './locked-versions'; import { detectMonorepos } from './monorepo'; import type { NpmPackage, NpmPackageDependency } from './types'; import { isZeroInstall } from './yarn'; +import { + YarnConfig, + loadConfigFromYarnrcYml, + resolveRegistryUrl, +} from './yarnrc'; function parseDepName(depType: string, key: string): string { if (depType !== 'resolutions') { @@ -138,6 +143,12 @@ export async function extractPackageFile( const yarnrcYmlFileName = getSiblingFileName(fileName, '.yarnrc.yml'); const yarnZeroInstall = await isZeroInstall(yarnrcYmlFileName); + let yarnConfig: YarnConfig | null = null; + const repoYarnrcYml = await readLocalFile(yarnrcYmlFileName, 'utf8'); + if (is.string(repoYarnrcYml)) { + yarnConfig = loadConfigFromYarnrcYml(repoYarnrcYml); + } + let lernaJsonFile: string | undefined; let lernaPackages: string[] | undefined; let lernaClient: 'yarn' | 'npm' | undefined; @@ -272,6 +283,12 @@ export async function extractPackageFile( hasFancyRefs = true; return dep; } + if (yarnConfig) { + const registryUrlFromYarnConfig = resolveRegistryUrl(depName, yarnConfig); + if (registryUrlFromYarnConfig) { + dep.registryUrls = [registryUrlFromYarnConfig]; + } + } if (isValid(dep.currentValue)) { dep.datasource = NpmDatasource.id; if (dep.currentValue === '') { diff --git a/lib/modules/manager/npm/extract/yarnrc.spec.ts b/lib/modules/manager/npm/extract/yarnrc.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..499741626072c582195f727559d4794a64b388d5 --- /dev/null +++ b/lib/modules/manager/npm/extract/yarnrc.spec.ts @@ -0,0 +1,100 @@ +import { codeBlock } from 'common-tags'; +import { loadConfigFromYarnrcYml, resolveRegistryUrl } from './yarnrc'; + +describe('modules/manager/npm/extract/yarnrc', () => { + describe('resolveRegistryUrl()', () => { + it('considers default registry', () => { + const registryUrl = resolveRegistryUrl('a-package', { + npmRegistryServer: 'https://private.example.com/npm', + }); + expect(registryUrl).toBe('https://private.example.com/npm'); + }); + + it('chooses matching scoped registry over default registry', () => { + const registryUrl = resolveRegistryUrl('@scope/a-package', { + npmRegistryServer: 'https://private.example.com/npm', + npmScopes: { + scope: { + npmRegistryServer: 'https://scope.example.com/npm', + }, + }, + }); + expect(registryUrl).toBe('https://scope.example.com/npm'); + }); + + it('ignores non matching scoped registry', () => { + const registryUrl = resolveRegistryUrl('@scope/a-package', { + npmScopes: { + 'other-scope': { + npmRegistryServer: 'https://other-scope.example.com/npm', + }, + }, + }); + expect(registryUrl).toBeNull(); + }); + + it('ignores partial scope match', () => { + const registryUrl = resolveRegistryUrl('@scope-2/a-package', { + npmScopes: { + scope: { + npmRegistryServer: 'https://scope.example.com/npm', + }, + }, + }); + expect(registryUrl).toBeNull(); + }); + }); + + describe('loadConfigFromYarnrcYml()', () => { + it.each([ + [ + 'npmRegistryServer: https://npm.example.com', + { npmRegistryServer: 'https://npm.example.com' }, + ], + [ + codeBlock` + npmRegistryServer: https://npm.example.com + npmScopes: + foo: + npmRegistryServer: https://npm-foo.example.com + `, + { + npmRegistryServer: 'https://npm.example.com', + npmScopes: { + foo: { + npmRegistryServer: 'https://npm-foo.example.com', + }, + }, + }, + ], + [ + codeBlock` + npmRegistryServer: https://npm.example.com + nodeLinker: pnp + `, + { npmRegistryServer: 'https://npm.example.com' }, + ], + ['npmRegistryServer: 42', null], + ['npmScopes: 42', null], + [ + codeBlock` + npmScopes: + foo: 42 + `, + null, + ], + [ + codeBlock` + npmScopes: + foo: + npmRegistryServer: 42 + `, + null, + ], + ])('produces expected config (%s)', (yarnrcYml, expectedConfig) => { + const config = loadConfigFromYarnrcYml(yarnrcYml); + + expect(config).toEqual(expectedConfig); + }); + }); +}); diff --git a/lib/modules/manager/npm/extract/yarnrc.ts b/lib/modules/manager/npm/extract/yarnrc.ts new file mode 100644 index 0000000000000000000000000000000000000000..81f344851831a6b6933a439df2bd83eaed48e474 --- /dev/null +++ b/lib/modules/manager/npm/extract/yarnrc.ts @@ -0,0 +1,46 @@ +import { load } from 'js-yaml'; +import { z } from 'zod'; +import { logger } from '../../../../logger'; + +const YarnrcYmlSchema = z.object({ + npmRegistryServer: z.string().optional(), + npmScopes: z + .record( + z.object({ + npmRegistryServer: z.string().optional(), + }) + ) + .optional(), +}); + +export type YarnConfig = z.infer<typeof YarnrcYmlSchema>; + +export function loadConfigFromYarnrcYml(yarnrcYml: string): YarnConfig | null { + try { + return YarnrcYmlSchema.parse( + load(yarnrcYml, { + json: true, + }) + ); + } catch (err) { + logger.warn({ yarnrcYml, err }, `Failed to load yarnrc file`); + return null; + } +} + +export function resolveRegistryUrl( + packageName: string, + yarnConfig: YarnConfig +): string | null { + if (yarnConfig.npmScopes) { + for (const scope in yarnConfig.npmScopes) { + if (packageName.startsWith(`@${scope}/`)) { + return yarnConfig.npmScopes[scope].npmRegistryServer ?? null; + } + } + } + if (yarnConfig.npmRegistryServer) { + return yarnConfig.npmRegistryServer; + } + return null; +}