Skip to content

Commit

Permalink
feat(managers/npm): support pnpm catalogs (#33376)
Browse files Browse the repository at this point in the history
Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>
  • Loading branch information
fpapado and secustor authored Jan 28, 2025
1 parent 59e1e89 commit 0f06866
Show file tree
Hide file tree
Showing 19 changed files with 1,285 additions and 47 deletions.
2 changes: 1 addition & 1 deletion lib/config/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('config/index', () => {
const parentConfig = { ...defaultConfig };
const config = getManagerConfig(parentConfig, 'npm');
expect(config).toContainEntries([
['fileMatch', ['(^|/)package\\.json$']],
['fileMatch', ['(^|/)package\\.json$', '(^|/)pnpm-workspace\\.yaml$']],
]);
expect(getManagerConfig(parentConfig, 'html')).toContainEntries([
['fileMatch', ['\\.html?$']],
Expand Down
30 changes: 30 additions & 0 deletions lib/modules/manager/npm/extract/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,36 @@ describe('modules/manager/npm/extract/index', () => {
},
]);
});

it('extracts pnpm workspace yaml files', async () => {
fs.readLocalFile.mockResolvedValueOnce(codeBlock`
packages:
- pkg-a
catalog:
is-positive: 1.0.0
`);
const res = await extractAllPackageFiles(defaultExtractConfig, [
'pnpm-workspace.yaml',
]);
expect(res).toEqual([
{
deps: [
{
currentValue: '1.0.0',
datasource: 'npm',
depName: 'is-positive',
depType: 'pnpm.catalog.default',
prettyDepType: 'pnpm.catalog.default',
},
],
managerData: {
pnpmShrinkwrap: undefined,
},
packageFile: 'pnpm-workspace.yaml',
},
]);
});
});

describe('.postExtract()', () => {
Expand Down
32 changes: 27 additions & 5 deletions lib/modules/manager/npm/extract/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
import type { NpmLockFiles, NpmManagerData } from '../types';
import { getExtractedConstraints } from './common/dependency';
import { extractPackageJson } from './common/package-file';
import { extractPnpmWorkspaceFile, tryParsePnpmWorkspaceYaml } from './pnpm';
import { postExtract } from './post';
import type { NpmPackage } from './types';
import { isZeroInstall } from './yarn';
Expand Down Expand Up @@ -229,12 +230,33 @@ export async function extractAllPackageFiles(
const content = await readLocalFile(packageFile, 'utf8');
// istanbul ignore else
if (content) {
const deps = await extractPackageFile(content, packageFile, config);
if (deps) {
npmFiles.push({
...deps,
// pnpm workspace files are their own package file, defined via fileMatch.
// We duck-type the content here, to allow users to rename the file itself.
const parsedPnpmWorkspaceYaml = tryParsePnpmWorkspaceYaml(content);
if (parsedPnpmWorkspaceYaml.success) {
logger.trace(
{ packageFile },
`Extracting file as a pnpm workspace YAML file`,
);
const deps = await extractPnpmWorkspaceFile(
parsedPnpmWorkspaceYaml.data,
packageFile,
});
);
if (deps) {
npmFiles.push({
...deps,
packageFile,
});
}
} else {
logger.trace({ packageFile }, `Extracting as a package.json file`);
const deps = await extractPackageFile(content, packageFile, config);
if (deps) {
npmFiles.push({
...deps,
packageFile,
});
}
}
} else {
logger.debug({ packageFile }, `No content found`);
Expand Down
163 changes: 163 additions & 0 deletions lib/modules/manager/npm/extract/pnpm.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { codeBlock } from 'common-tags';
import { Fixtures } from '../../../../../test/fixtures';
import { fs, getFixturePath, logger, partial } from '../../../../../test/util';
import { GlobalConfig } from '../../../../config/global';
Expand All @@ -7,6 +8,7 @@ import type { NpmManagerData } from '../types';
import {
detectPnpmWorkspaces,
extractPnpmFilters,
extractPnpmWorkspaceFile,
findPnpmWorkspace,
getPnpmLock,
} from './pnpm';
Expand Down Expand Up @@ -278,10 +280,171 @@ describe('modules/manager/npm/extract/pnpm', () => {
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',
},
});
});
});
});
Loading

0 comments on commit 0f06866

Please # to comment.