0
0
Fork 0
mirror of https://github.com/renovatebot/renovate.git synced 2025-05-13 08:01:49 +00:00
renovatebot_renovate/lib/modules/manager/npm/extract/pnpm.spec.ts
2025-03-13 17:52:09 +00:00

450 lines
14 KiB
TypeScript

import { codeBlock } from 'common-tags';
import { GlobalConfig } from '../../../../config/global';
import * as yaml from '../../../../util/yaml';
import type { PackageFile } from '../../types';
import type { NpmManagerData } from '../types';
import {
detectPnpmWorkspaces,
extractPnpmFilters,
extractPnpmWorkspaceFile,
findPnpmWorkspace,
getPnpmLock,
} from './pnpm';
import { Fixtures } from '~test/fixtures';
import { fs, getFixturePath, logger, partial } from '~test/util';
vi.mock('../../../../util/fs');
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');
// 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: { pnpmShrinkwrap: 'pnpm-lock.yaml' },
},
{
packageFile: 'nested-packages/group/a/package.json',
managerData: {
pnpmShrinkwrap: undefined,
packageJsonName: '@demo/nested-group-a',
},
},
{
packageFile: 'nested-packages/group/b/package.json',
managerData: {
pnpmShrinkwrap: undefined,
packageJsonName: '@demo/nested-group-b',
},
},
{
packageFile: 'non-nested-packages/a/package.json',
managerData: {
pnpmShrinkwrap: undefined,
packageJsonName: '@demo/non-nested-a',
},
},
{
packageFile: 'non-nested-packages/b/package.json',
managerData: {
pnpmShrinkwrap: undefined,
packageJsonName: '@demo/non-nested-b',
},
},
{
packageFile: 'solo-package/package.json',
managerData: {
pnpmShrinkwrap: undefined,
packageJsonName: '@demo/solo',
},
},
{
packageFile: 'solo-package-leading-dot-slash/package.json',
managerData: {
pnpmShrinkwrap: undefined,
packageJsonName: '@demo/solo-leading-dot-slash',
},
},
{
packageFile: 'solo-package-leading-double-dot-slash/package.json',
managerData: {
pnpmShrinkwrap: undefined,
packageJsonName: '@demo/solo-leading-double-dot-slash',
},
},
{
packageFile: 'solo-package-trailing-slash/package.json',
managerData: {
pnpmShrinkwrap: undefined,
packageJsonName: '@demo/solo-trailing-slash',
},
},
{
packageFile: 'test/test-package/package.json',
managerData: {
pnpmShrinkwrap: undefined,
packageJsonName: '@demo/test-package',
},
},
{
packageFile: 'tests/test-package2/package.json',
managerData: {
pnpmShrinkwrap: undefined,
packageJsonName: '@demo/test-package2',
},
},
]);
await detectPnpmWorkspaces(packageFiles);
expect(packageFiles).toMatchSnapshot();
expect(
packageFiles.every(
(packageFile) =>
packageFile.managerData?.pnpmShrinkwrap !== undefined,
),
).toBeTrue();
});
it('skips when pnpm shrinkwrap file has already been provided', async () => {
const packageFiles = [
{
packageFile: 'package.json',
managerData: { pnpmShrinkwrap: 'pnpm-lock.yaml' },
},
];
await detectPnpmWorkspaces(packageFiles);
expect(packageFiles).toEqual([
{
packageFile: 'package.json',
managerData: { pnpmShrinkwrap: 'pnpm-lock.yaml' },
},
]);
});
it('filters none matching packages', async () => {
fs.localPathExists.mockResolvedValue(true);
const packageFiles = [
{
packageFile: 'package.json',
managerData: { pnpmShrinkwrap: 'pnpm-lock.yaml' },
},
{
packageFile: 'nested-packages/group/a/package.json',
packageJsonName: '@demo/nested-group-a',
managerData: { pnpmShrinkwrap: undefined },
},
{
packageFile: 'not-matching/b/package.json',
packageJsonName: '@not-matching/b',
managerData: { pnpmShrinkwrap: undefined },
},
];
await detectPnpmWorkspaces(packageFiles);
expect(packageFiles).toEqual([
{
packageFile: 'package.json',
managerData: { pnpmShrinkwrap: 'pnpm-lock.yaml' },
},
{
packageFile: 'nested-packages/group/a/package.json',
packageJsonName: '@demo/nested-group-a',
managerData: { pnpmShrinkwrap: 'pnpm-lock.yaml' },
},
{
packageFile: 'not-matching/b/package.json',
packageJsonName: '@not-matching/b',
managerData: { pnpmShrinkwrap: undefined },
},
]);
expect(
packageFiles.find(
(packageFile) =>
packageFile.packageFile === 'not-matching/b/package.json',
)?.managerData.pnpmShrinkwrap,
).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();
});
});
describe('.extractPnpmWorkspaceFile()', () => {
it('handles empty catalog entries', async () => {
expect(
await extractPnpmWorkspaceFile(
{ catalog: {}, catalogs: {} },
'pnpm-workspace.yaml',
),
).toMatchObject({
deps: [],
});
});
it('parses valid pnpm-workspace.yaml file', async () => {
expect(
await extractPnpmWorkspaceFile(
{
catalog: {
react: '18.3.0',
},
catalogs: {
react17: {
react: '17.0.2',
},
},
},
'pnpm-workspace.yaml',
),
).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('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');
expect(
await extractPnpmWorkspaceFile(
{
catalog: {
react: '18.3.1',
},
},
'pnpm-workspace.yaml',
),
).toMatchObject({
managerData: {
pnpmShrinkwrap: 'pnpm-lock.yaml',
},
});
});
});
});