Skip to content

Commit

Permalink
feat(manager/npm): read registry URLs from .yarnrc.yml (#19864)
Browse files Browse the repository at this point in the history
Co-authored-by: Valentin Agachi <github-com@agachi.name>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
Closes #16353
  • Loading branch information
fgreinacher committed Jan 31, 2023
1 parent 458dfe0 commit 2143c97
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 5 deletions.
6 changes: 1 addition & 5 deletions lib/modules/manager/npm/__fixtures__/inputs/02.json
Expand Up @@ -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",
Expand Down
Expand Up @@ -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": {},
Expand Down
18 changes: 18 additions & 0 deletions lib/modules/manager/npm/extract/index.spec.ts
Expand Up @@ -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(
Expand Down Expand Up @@ -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') {
Expand Down
17 changes: 17 additions & 0 deletions lib/modules/manager/npm/extract/index.ts
Expand Up @@ -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') {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 === '') {
Expand Down
100 changes: 100 additions & 0 deletions 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);
});
});
});
46 changes: 46 additions & 0 deletions 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;
}

0 comments on commit 2143c97

Please sign in to comment.