mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 10:09:50 +00:00
a4a5efa8c2
pnpm v11.23 reshaped the `registries` setting in `pnpm-workspace.yaml`: it is now keyed by the registry URL, with the scopes routed to it listed in the entry. Renovate only accepted the older `<scope>: <url>` shape, so parsing of the whole workspace file failed and its catalogs and overrides were no longer extracted at all. Accept both shapes and flatten the new one into the old one, where the bare `@` scope is the scope-less default registry. URL keys which interpolate an env var, embed credentials, or are unparseable are skipped, matching what pnpm itself ignores or refuses. A `registries` value which is neither shape now falls back to `undefined` instead of taking the file's catalogs with it. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
802 lines
23 KiB
TypeScript
802 lines
23 KiB
TypeScript
import { codeBlock } from 'common-tags';
|
|
import { Fixtures } from '~test/fixtures.ts';
|
|
import { fs, getFixturePath, logger, partial } from '~test/util.ts';
|
|
import { GlobalConfig } from '../../../../config/global.ts';
|
|
import * as yaml from '../../../../util/yaml.ts';
|
|
import type { PackageFile } from '../../types.ts';
|
|
import { PnpmWorkspaceFile } from '../schema.ts';
|
|
import type { NpmManagerData } from '../types.ts';
|
|
import {
|
|
detectPnpmWorkspaces,
|
|
extractPnpmFilters,
|
|
extractPnpmWorkspaceFile,
|
|
findPnpmWorkspace,
|
|
getPnpmLock,
|
|
resolveRegistryUrl,
|
|
} from './pnpm.ts';
|
|
|
|
vi.mock('../../../../util/fs/index.ts');
|
|
|
|
describe('modules/manager/npm/extract/pnpm', () => {
|
|
beforeAll(() => {
|
|
GlobalConfig.set({ localDir: getFixturePath('pnpm-monorepo/', '..') });
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('.extractPnpmFilters()', () => {
|
|
it('detects errors in pnpm-workspace.yml file structure', async () => {
|
|
fs.readLocalFile.mockResolvedValueOnce('p!!!ckages:\n - "packages/*"');
|
|
|
|
const workSpaceFilePath = getFixturePath(
|
|
'pnpm-monorepo/pnpm-workspace.yml',
|
|
'..',
|
|
);
|
|
const res = await extractPnpmFilters(workSpaceFilePath);
|
|
expect(res).toBeUndefined();
|
|
|
|
expect(logger.logger.trace).toHaveBeenCalledWith(
|
|
{
|
|
fileName: expect.any(String),
|
|
},
|
|
'Failed to find required "packages" array in pnpm-workspace.yaml',
|
|
);
|
|
});
|
|
|
|
it('detects errors when opening pnpm-workspace.yml file', async () => {
|
|
vi.spyOn(yaml, 'parseSingleYaml').mockImplementationOnce(() => {
|
|
throw new Error();
|
|
});
|
|
|
|
const res = await extractPnpmFilters('pnpm-workspace.yml');
|
|
expect(res).toBeUndefined();
|
|
|
|
expect(logger.logger.trace).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
fileName: expect.any(String),
|
|
err: expect.anything(),
|
|
}),
|
|
'Failed to parse pnpm-workspace.yaml',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('.findPnpmWorkspace()', () => {
|
|
it('detects missing pnpm-workspace.yaml', async () => {
|
|
fs.findLocalSiblingOrParent.mockResolvedValueOnce(null);
|
|
|
|
const packageFile = 'package.json';
|
|
const res = await findPnpmWorkspace(packageFile);
|
|
expect(res).toBeNull();
|
|
|
|
expect(logger.logger.trace).toHaveBeenCalledWith(
|
|
expect.objectContaining({ packageFile }),
|
|
'Failed to locate pnpm-workspace.yaml in a parent directory.',
|
|
);
|
|
});
|
|
|
|
it('detects missing pnpm-lock.yaml when pnpm-workspace.yaml was already found', async () => {
|
|
fs.findLocalSiblingOrParent.mockResolvedValueOnce('pnpm-workspace.yaml');
|
|
fs.localPathExists.mockResolvedValueOnce(false);
|
|
|
|
const packageFile = 'package.json';
|
|
const res = await findPnpmWorkspace(packageFile);
|
|
expect(res).toBeNull();
|
|
|
|
expect(logger.logger.trace).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
workspaceYamlPath: 'pnpm-workspace.yaml',
|
|
packageFile,
|
|
}),
|
|
'Failed to find a pnpm-lock.yaml sibling for the workspace.',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('.detectPnpmWorkspaces()', () => {
|
|
beforeEach(async () => {
|
|
const realFs = await vi.importActual<typeof fs>(
|
|
'../../../../util/fs/index.ts',
|
|
);
|
|
|
|
// The real implementations of these functions are used for this block;
|
|
// they do static path manipulation.
|
|
fs.findLocalSiblingOrParent.mockImplementation(
|
|
realFs.findLocalSiblingOrParent,
|
|
);
|
|
fs.getSiblingFileName.mockImplementation(realFs.getSiblingFileName);
|
|
|
|
// Falls through to reading from the fixture path defined in GlobalConfig
|
|
// at the top of this file
|
|
fs.readLocalFile.mockImplementation(realFs.readLocalFile);
|
|
});
|
|
|
|
it('uses pnpm workspaces', async () => {
|
|
fs.localPathExists.mockResolvedValue(true);
|
|
const packageFiles = partial<PackageFile<NpmManagerData>>([
|
|
{
|
|
packageFile: 'package.json',
|
|
managerData: { pnpmLockFile: 'pnpm-lock.yaml' },
|
|
},
|
|
{
|
|
packageFile: 'nested-packages/group/a/package.json',
|
|
managerData: {
|
|
pnpmLockFile: undefined,
|
|
packageJsonName: '@demo/nested-group-a',
|
|
},
|
|
},
|
|
{
|
|
packageFile: 'nested-packages/group/b/package.json',
|
|
managerData: {
|
|
pnpmLockFile: undefined,
|
|
packageJsonName: '@demo/nested-group-b',
|
|
},
|
|
},
|
|
{
|
|
packageFile: 'non-nested-packages/a/package.json',
|
|
managerData: {
|
|
pnpmLockFile: undefined,
|
|
packageJsonName: '@demo/non-nested-a',
|
|
},
|
|
},
|
|
{
|
|
packageFile: 'non-nested-packages/b/package.json',
|
|
managerData: {
|
|
pnpmLockFile: undefined,
|
|
packageJsonName: '@demo/non-nested-b',
|
|
},
|
|
},
|
|
{
|
|
packageFile: 'solo-package/package.json',
|
|
managerData: {
|
|
pnpmLockFile: undefined,
|
|
packageJsonName: '@demo/solo',
|
|
},
|
|
},
|
|
{
|
|
packageFile: 'solo-package-leading-dot-slash/package.json',
|
|
managerData: {
|
|
pnpmLockFile: undefined,
|
|
packageJsonName: '@demo/solo-leading-dot-slash',
|
|
},
|
|
},
|
|
{
|
|
packageFile: 'solo-package-leading-double-dot-slash/package.json',
|
|
managerData: {
|
|
pnpmLockFile: undefined,
|
|
packageJsonName: '@demo/solo-leading-double-dot-slash',
|
|
},
|
|
},
|
|
{
|
|
packageFile: 'solo-package-trailing-slash/package.json',
|
|
managerData: {
|
|
pnpmLockFile: undefined,
|
|
packageJsonName: '@demo/solo-trailing-slash',
|
|
},
|
|
},
|
|
{
|
|
packageFile: 'test/test-package/package.json',
|
|
managerData: {
|
|
pnpmLockFile: undefined,
|
|
packageJsonName: '@demo/test-package',
|
|
},
|
|
},
|
|
{
|
|
packageFile: 'tests/test-package2/package.json',
|
|
managerData: {
|
|
pnpmLockFile: undefined,
|
|
packageJsonName: '@demo/test-package2',
|
|
},
|
|
},
|
|
]);
|
|
|
|
await detectPnpmWorkspaces(packageFiles);
|
|
// every workspace package file gets associated with the root lockfile
|
|
expect(packageFiles).toMatchObject(
|
|
packageFiles.map(() => ({
|
|
managerData: { pnpmLockFile: 'pnpm-lock.yaml' },
|
|
})),
|
|
);
|
|
expect(
|
|
packageFiles.every(
|
|
(packageFile) => packageFile.managerData?.pnpmLockFile !== undefined,
|
|
),
|
|
).toBeTrue();
|
|
});
|
|
|
|
it('skips when pnpm shrinkwrap file has already been provided', async () => {
|
|
const packageFiles = [
|
|
{
|
|
packageFile: 'package.json',
|
|
managerData: { pnpmLockFile: 'pnpm-lock.yaml' },
|
|
},
|
|
];
|
|
|
|
await detectPnpmWorkspaces(packageFiles);
|
|
expect(packageFiles).toEqual([
|
|
{
|
|
packageFile: 'package.json',
|
|
managerData: { pnpmLockFile: 'pnpm-lock.yaml' },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('skips packages without a pnpm workspace', async () => {
|
|
fs.localPathExists.mockResolvedValue(false);
|
|
const packageFiles = [
|
|
{
|
|
packageFile: 'nested-packages/group/a/package.json',
|
|
packageJsonName: '@demo/nested-group-a',
|
|
managerData: { pnpmLockFile: undefined },
|
|
},
|
|
];
|
|
|
|
await detectPnpmWorkspaces(packageFiles);
|
|
expect(packageFiles[0].managerData.pnpmLockFile).toBeUndefined();
|
|
});
|
|
|
|
it('filters none matching packages', async () => {
|
|
fs.localPathExists.mockResolvedValue(true);
|
|
const packageFiles = [
|
|
{
|
|
packageFile: 'package.json',
|
|
managerData: { pnpmLockFile: 'pnpm-lock.yaml' },
|
|
},
|
|
{
|
|
packageFile: 'nested-packages/group/a/package.json',
|
|
packageJsonName: '@demo/nested-group-a',
|
|
managerData: { pnpmLockFile: undefined },
|
|
},
|
|
{
|
|
packageFile: 'not-matching/b/package.json',
|
|
packageJsonName: '@not-matching/b',
|
|
managerData: { pnpmLockFile: undefined },
|
|
},
|
|
];
|
|
|
|
await detectPnpmWorkspaces(packageFiles);
|
|
expect(packageFiles).toEqual([
|
|
{
|
|
packageFile: 'package.json',
|
|
managerData: { pnpmLockFile: 'pnpm-lock.yaml' },
|
|
},
|
|
{
|
|
packageFile: 'nested-packages/group/a/package.json',
|
|
packageJsonName: '@demo/nested-group-a',
|
|
managerData: { pnpmLockFile: 'pnpm-lock.yaml' },
|
|
},
|
|
{
|
|
packageFile: 'not-matching/b/package.json',
|
|
packageJsonName: '@not-matching/b',
|
|
managerData: { pnpmLockFile: undefined },
|
|
},
|
|
]);
|
|
expect(
|
|
packageFiles.find(
|
|
(packageFile) =>
|
|
packageFile.packageFile === 'not-matching/b/package.json',
|
|
)?.managerData.pnpmLockFile,
|
|
).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('.getPnpmLock()', () => {
|
|
it('returns empty if failed to parse', async () => {
|
|
fs.readLocalFile.mockResolvedValueOnce(undefined as never);
|
|
const res = await getPnpmLock('package.json');
|
|
expect(res.lockedVersionsWithPath).toBeUndefined();
|
|
});
|
|
|
|
it('extracts version from monorepo', async () => {
|
|
const plocktest1Lock = Fixtures.get('pnpm-monorepo/pnpm-lock.yaml', '..');
|
|
fs.readLocalFile.mockResolvedValueOnce(plocktest1Lock);
|
|
const res = await getPnpmLock('package.json');
|
|
expect(Object.keys(res.lockedVersionsWithPath!)).toHaveLength(11);
|
|
});
|
|
|
|
it('extracts version from normal repo', async () => {
|
|
const plocktest1Lock = Fixtures.get(
|
|
'lockfile-parsing/pnpm-lock.yaml',
|
|
'..',
|
|
);
|
|
fs.readLocalFile.mockResolvedValueOnce(plocktest1Lock);
|
|
const res = await getPnpmLock('package.json');
|
|
expect(Object.keys(res.lockedVersionsWithPath!)).toHaveLength(1);
|
|
});
|
|
|
|
it('extracts version from catalogs', async () => {
|
|
const lockfileContent = codeBlock`
|
|
lockfileVersion: '9.0'
|
|
|
|
settings:
|
|
autoInstallPeers: true
|
|
excludeLinksFromLockfile: false
|
|
|
|
catalogs:
|
|
default:
|
|
react:
|
|
specifier: ^18
|
|
version: 18.3.1
|
|
|
|
importers:
|
|
|
|
.:
|
|
dependencies:
|
|
react:
|
|
specifier: 'catalog:'
|
|
version: 18.3.1
|
|
|
|
packages:
|
|
|
|
js-tokens@4.0.0:
|
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
|
|
|
loose-envify@1.4.0:
|
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
|
hasBin: true
|
|
|
|
react@18.3.1:
|
|
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
|
engines: {node: '>=0.10.0'}
|
|
|
|
snapshots:
|
|
|
|
js-tokens@4.0.0: {}
|
|
|
|
loose-envify@1.4.0:
|
|
dependencies:
|
|
js-tokens: 4.0.0
|
|
|
|
react@18.3.1:
|
|
dependencies:
|
|
loose-envify: 1.4.0
|
|
`;
|
|
fs.readLocalFile.mockResolvedValueOnce(lockfileContent);
|
|
const res = await getPnpmLock('package.json');
|
|
expect(Object.keys(res.lockedVersionsWithCatalog!)).toHaveLength(1);
|
|
});
|
|
|
|
it('returns empty if no deps', async () => {
|
|
fs.readLocalFile.mockResolvedValueOnce('{}');
|
|
const res = await getPnpmLock('package.json');
|
|
expect(res.lockedVersionsWithPath).toBeUndefined();
|
|
});
|
|
|
|
it('extracts version from multi-document lockfile (configDependencies)', async () => {
|
|
const lockfileContent = codeBlock`
|
|
---
|
|
lockfileVersion: '9.0'
|
|
|
|
importers:
|
|
|
|
.:
|
|
configDependencies:
|
|
'@scope/pnpm-plugin-defaults':
|
|
specifier: 0.1.0
|
|
version: 0.1.0
|
|
|
|
packages:
|
|
|
|
'@scope/pnpm-plugin-defaults@0.1.0':
|
|
resolution: {integrity: sha512-aaa==}
|
|
|
|
snapshots:
|
|
|
|
'@scope/pnpm-plugin-defaults@0.1.0': {}
|
|
|
|
---
|
|
lockfileVersion: '9.0'
|
|
|
|
settings:
|
|
autoInstallPeers: true
|
|
excludeLinksFromLockfile: false
|
|
|
|
importers:
|
|
|
|
.:
|
|
dependencies:
|
|
lodash:
|
|
specifier: ^4.17.20
|
|
version: 4.18.1
|
|
|
|
packages:
|
|
|
|
lodash@4.18.1:
|
|
resolution: {integrity: sha512-bbb==}
|
|
|
|
snapshots:
|
|
|
|
lodash@4.18.1: {}
|
|
`;
|
|
fs.readLocalFile.mockResolvedValueOnce(lockfileContent);
|
|
const res = await getPnpmLock('package.json');
|
|
expect(Object.keys(res.lockedVersionsWithPath!)).toHaveLength(1);
|
|
expect(res.lockedVersionsWithPath!['.'].dependencies.lodash).toBe(
|
|
'4.18.1',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('.extractPnpmWorkspaceFile()', () => {
|
|
it('handles empty catalog entries', async () => {
|
|
await expect(
|
|
extractPnpmWorkspaceFile(
|
|
{ catalog: {}, catalogs: {} },
|
|
'pnpm-workspace.yaml',
|
|
),
|
|
).resolves.toMatchObject({
|
|
deps: [],
|
|
});
|
|
});
|
|
|
|
it('parses valid pnpm-workspace.yaml file', async () => {
|
|
await expect(
|
|
extractPnpmWorkspaceFile(
|
|
{
|
|
catalog: {
|
|
react: '18.3.0',
|
|
},
|
|
catalogs: {
|
|
react17: {
|
|
react: '17.0.2',
|
|
},
|
|
},
|
|
},
|
|
'pnpm-workspace.yaml',
|
|
),
|
|
).resolves.toMatchObject({
|
|
deps: [
|
|
{
|
|
currentValue: '18.3.0',
|
|
datasource: 'npm',
|
|
depName: 'react',
|
|
depType: 'pnpm.catalog.default',
|
|
prettyDepType: 'pnpm.catalog.default',
|
|
},
|
|
{
|
|
currentValue: '17.0.2',
|
|
datasource: 'npm',
|
|
depName: 'react',
|
|
depType: 'pnpm.catalog.react17',
|
|
prettyDepType: 'pnpm.catalog.react17',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('parses overrides in pnpm-workspace.yaml file', async () => {
|
|
await expect(
|
|
extractPnpmWorkspaceFile(
|
|
{
|
|
overrides: {
|
|
'foo>bar': '2.0.0',
|
|
'foo@1.0.0': '2.0.0',
|
|
'foo@>1.0.0': '2.0.0',
|
|
'foo@>=1.0.0': '2.0.0',
|
|
'foo@1.0.0>bar': '2.0.0',
|
|
'foo@>1.0.0>bar': '2.0.0',
|
|
'foo@>=1.0.0 <2.0.0': '>=2.0.0',
|
|
},
|
|
},
|
|
'pnpm-workspace.yaml',
|
|
),
|
|
).resolves.toMatchObject({
|
|
deps: [
|
|
{
|
|
currentValue: '2.0.0',
|
|
datasource: 'npm',
|
|
depName: 'foo>bar',
|
|
depType: 'pnpm-workspace.overrides',
|
|
packageName: 'bar',
|
|
},
|
|
{
|
|
currentValue: '2.0.0',
|
|
datasource: 'npm',
|
|
depName: 'foo@1.0.0',
|
|
depType: 'pnpm-workspace.overrides',
|
|
packageName: 'foo',
|
|
},
|
|
{
|
|
currentValue: '2.0.0',
|
|
datasource: 'npm',
|
|
depName: 'foo@>1.0.0',
|
|
depType: 'pnpm-workspace.overrides',
|
|
packageName: 'foo',
|
|
},
|
|
{
|
|
currentValue: '2.0.0',
|
|
datasource: 'npm',
|
|
depName: 'foo@>=1.0.0',
|
|
depType: 'pnpm-workspace.overrides',
|
|
packageName: 'foo',
|
|
},
|
|
{
|
|
currentValue: '2.0.0',
|
|
datasource: 'npm',
|
|
depName: 'foo@1.0.0>bar',
|
|
depType: 'pnpm-workspace.overrides',
|
|
packageName: 'bar',
|
|
},
|
|
{
|
|
currentValue: '2.0.0',
|
|
datasource: 'npm',
|
|
depName: 'foo@>1.0.0>bar',
|
|
depType: 'pnpm-workspace.overrides',
|
|
packageName: 'bar',
|
|
},
|
|
{
|
|
currentValue: '>=2.0.0',
|
|
datasource: 'npm',
|
|
depName: 'foo@>=1.0.0 <2.0.0',
|
|
depType: 'pnpm-workspace.overrides',
|
|
packageName: 'foo',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('applies scoped registry from registries to catalog deps', async () => {
|
|
await expect(
|
|
extractPnpmWorkspaceFile(
|
|
{
|
|
catalog: {
|
|
'@my-org/pkg': '1.0.0',
|
|
react: '18.3.0',
|
|
},
|
|
registries: {
|
|
'@my-org': 'https://private.example.com/',
|
|
},
|
|
},
|
|
'pnpm-workspace.yaml',
|
|
),
|
|
).resolves.toMatchObject({
|
|
deps: [
|
|
{
|
|
depName: '@my-org/pkg',
|
|
registryUrls: ['https://private.example.com/'],
|
|
},
|
|
{
|
|
depName: 'react',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('applies top-level registry to all npm catalog deps', async () => {
|
|
const res = await extractPnpmWorkspaceFile(
|
|
{
|
|
catalog: {
|
|
'@my-org/pkg': '1.0.0',
|
|
react: '18.3.0',
|
|
},
|
|
registry: 'https://private.example.com/',
|
|
},
|
|
'pnpm-workspace.yaml',
|
|
);
|
|
expect(res?.deps.flatMap((dep) => dep.registryUrls)).toEqual([
|
|
'https://private.example.com/',
|
|
'https://private.example.com/',
|
|
]);
|
|
});
|
|
|
|
it('strips registries values containing env vars when parsing', async () => {
|
|
const parsed = await PnpmWorkspaceFile.safeParseAsync(codeBlock`
|
|
registries:
|
|
"@my-org": https://\${TOKEN}.example.com/
|
|
"@other-org": https://private.example.com/
|
|
`);
|
|
expect(parsed.data?.registries).toEqual({
|
|
'@other-org': 'https://private.example.com/',
|
|
});
|
|
});
|
|
|
|
it('applies url-keyed registries to catalog deps', async () => {
|
|
const parsed = await PnpmWorkspaceFile.safeParseAsync(codeBlock`
|
|
catalog:
|
|
"@my-org/pkg": 1.0.0
|
|
react: 18.3.0
|
|
registries:
|
|
https://private.example.com/:
|
|
serverType: artifactory
|
|
scopes: ["@my-org", "@my-other-org"]
|
|
prefix: work
|
|
https://default.example.com/:
|
|
scopes: ["@"]
|
|
supportsTimeField: true
|
|
`);
|
|
|
|
await expect(
|
|
extractPnpmWorkspaceFile(parsed.data!, 'pnpm-workspace.yaml'),
|
|
).resolves.toMatchObject({
|
|
deps: [
|
|
{
|
|
depName: '@my-org/pkg',
|
|
registryUrls: ['https://private.example.com/'],
|
|
},
|
|
{
|
|
depName: 'react',
|
|
registryUrls: ['https://default.example.com/'],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('skips unusable url-keyed registries when parsing', async () => {
|
|
const parsed = await PnpmWorkspaceFile.safeParseAsync(codeBlock`
|
|
registries:
|
|
https://\${TOKEN}.example.com/:
|
|
scopes: ["@env-var-org"]
|
|
https://user:pass@credentials.example.com/:
|
|
scopes: ["@credentials-org"]
|
|
not-a-url:
|
|
scopes: ["@invalid-url-org"]
|
|
https://unrouted.example.com/:
|
|
https://private.example.com/:
|
|
scopes: ["@my-org"]
|
|
`);
|
|
|
|
expect(parsed.data?.registries).toEqual({
|
|
'@my-org': 'https://private.example.com/',
|
|
});
|
|
});
|
|
|
|
it('ignores registries which mix both shapes', async () => {
|
|
const parsed = await PnpmWorkspaceFile.safeParseAsync(codeBlock`
|
|
catalog:
|
|
react: 18.3.0
|
|
registries:
|
|
"@my-org": https://private.example.com/
|
|
https://default.example.com/:
|
|
scopes: ["@"]
|
|
`);
|
|
|
|
expect(parsed.data?.registries).toBeUndefined();
|
|
expect(parsed.data?.catalog).toEqual({ react: '18.3.0' });
|
|
});
|
|
|
|
it('does not apply registries to non-npm catalog deps', async () => {
|
|
const res = await extractPnpmWorkspaceFile(
|
|
{
|
|
catalog: {
|
|
a: 'github:owner/a#v1.1.0',
|
|
},
|
|
registry: 'https://private.example.com/',
|
|
},
|
|
'pnpm-workspace.yaml',
|
|
);
|
|
expect(res?.deps).toMatchObject([
|
|
{ depName: 'a', datasource: 'github-tags' },
|
|
]);
|
|
expect(res?.deps[0].registryUrls).toBeUndefined();
|
|
});
|
|
|
|
it('resolves override registry using the package name, not the selector', async () => {
|
|
// depName starts with `@my-org/` but the real target package is `bar`,
|
|
// so the `@my-org` scoped registry must NOT apply.
|
|
const res = await extractPnpmWorkspaceFile(
|
|
{
|
|
overrides: {
|
|
'@my-org/foo>bar': '2.0.0',
|
|
},
|
|
registries: {
|
|
'@my-org': 'https://private.example.com/',
|
|
},
|
|
},
|
|
'pnpm-workspace.yaml',
|
|
);
|
|
expect(res?.deps).toMatchObject([
|
|
{
|
|
depName: '@my-org/foo>bar',
|
|
packageName: 'bar',
|
|
},
|
|
]);
|
|
expect(res?.deps[0].registryUrls).toBeUndefined();
|
|
});
|
|
|
|
it('finds relevant lockfile', async () => {
|
|
const lockfileContent = codeBlock`
|
|
lockfileVersion: '9.0'
|
|
|
|
catalogs:
|
|
default:
|
|
react:
|
|
specifier: 18.3.1
|
|
version: 18.3.1
|
|
|
|
importers:
|
|
|
|
.:
|
|
dependencies:
|
|
react:
|
|
specifier: 'catalog:'
|
|
version: 18.3.1
|
|
|
|
packages:
|
|
|
|
js-tokens@4.0.0:
|
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
|
|
|
loose-envify@1.4.0:
|
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
|
hasBin: true
|
|
|
|
react@18.3.1:
|
|
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
|
engines: {node: '>=0.10.0'}
|
|
|
|
snapshots:
|
|
|
|
js-tokens@4.0.0: {}
|
|
|
|
loose-envify@1.4.0:
|
|
dependencies:
|
|
js-tokens: 4.0.0
|
|
|
|
react@18.3.1:
|
|
dependencies:
|
|
loose-envify: 1.4.0
|
|
`;
|
|
fs.readLocalFile.mockResolvedValueOnce(lockfileContent);
|
|
fs.getSiblingFileName.mockReturnValueOnce('pnpm-lock.yaml');
|
|
await expect(
|
|
extractPnpmWorkspaceFile(
|
|
{
|
|
catalog: {
|
|
react: '18.3.1',
|
|
},
|
|
},
|
|
'pnpm-workspace.yaml',
|
|
),
|
|
).resolves.toMatchObject({
|
|
managerData: {
|
|
pnpmLockFile: 'pnpm-lock.yaml',
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('.resolveRegistryUrl()', () => {
|
|
it('returns the scoped registry for a matching scope', () => {
|
|
expect(
|
|
resolveRegistryUrl(
|
|
'@my-org/pkg',
|
|
{ '@my-org': 'https://private.example.com/' },
|
|
undefined,
|
|
),
|
|
).toBe('https://private.example.com/');
|
|
});
|
|
|
|
it('falls back to registries.default for an unscoped package', () => {
|
|
expect(
|
|
resolveRegistryUrl(
|
|
'react',
|
|
{ default: 'https://default.example.com/' },
|
|
undefined,
|
|
),
|
|
).toBe('https://default.example.com/');
|
|
});
|
|
|
|
it('falls back to the default registry for a non-matching scope', () => {
|
|
expect(
|
|
resolveRegistryUrl(
|
|
'@other/pkg',
|
|
{ '@my-org': 'https://private.example.com/' },
|
|
'https://default.example.com/',
|
|
),
|
|
).toBe('https://default.example.com/');
|
|
});
|
|
|
|
it('falls back to the top-level registry when no registries are set', () => {
|
|
expect(
|
|
resolveRegistryUrl('react', undefined, 'https://default.example.com/'),
|
|
).toBe('https://default.example.com/');
|
|
});
|
|
|
|
it('returns null when nothing matches', () => {
|
|
expect(resolveRegistryUrl('react', undefined, undefined)).toBeNull();
|
|
});
|
|
});
|
|
});
|