diff --git a/lib/modules/manager/npm/extract/locked-versions.spec.ts b/lib/modules/manager/npm/extract/locked-versions.spec.ts index 6f411397e710f0bed56c34ed55065a026824ce37..bbac5fcacafef5619c5528fd5e874191a0beaa13 100644 --- a/lib/modules/manager/npm/extract/locked-versions.spec.ts +++ b/lib/modules/manager/npm/extract/locked-versions.spec.ts @@ -520,11 +520,10 @@ describe('modules/manager/npm/extract/locked-versions', () => { packageFile: 'some-file', }, ]; - pnpm.getConstraints.mockReturnValue('>=6.0.0 >=8'); await getLockedVersions(packageFiles); expect(packageFiles).toEqual([ { - extractedConstraints: { pnpm: '>=6.0.0 >=8' }, + extractedConstraints: { pnpm: '>=6.0.0' }, deps: [ { currentValue: '1.0.0', depName: 'a', lockedVersion: '1.0.0' }, { currentValue: '2.0.0', depName: 'b', lockedVersion: '2.0.0' }, diff --git a/lib/modules/manager/npm/extract/locked-versions.ts b/lib/modules/manager/npm/extract/locked-versions.ts index 42896752400b3ea94d1843d761df056b4d36eada..0515deab74de2d139ffe72c9c874b6b4665030f3 100644 --- a/lib/modules/manager/npm/extract/locked-versions.ts +++ b/lib/modules/manager/npm/extract/locked-versions.ts @@ -3,7 +3,7 @@ import { logger } from '../../../../logger'; import type { PackageFile } from '../../types'; import type { NpmManagerData } from '../types'; import { getNpmLock } from './npm'; -import { getConstraints, getPnpmLock } from './pnpm'; +import { getPnpmLock } from './pnpm'; import type { LockFile } from './types'; import { getYarnLock } from './yarn'; @@ -106,14 +106,6 @@ export async function getLockedVersions( logger.trace(`Retrieving/parsing ${pnpmShrinkwrap}`); lockFileCache[pnpmShrinkwrap] = await getPnpmLock(pnpmShrinkwrap); } - const { lockfileVersion } = lockFileCache[pnpmShrinkwrap]; - if (lockfileVersion) { - packageFile.extractedConstraints ??= {}; - packageFile.extractedConstraints.pnpm = getConstraints( - lockfileVersion, - packageFile.extractedConstraints.pnpm - ); - } for (const dep of packageFile.deps) { // TODO: types (#7154) diff --git a/lib/modules/manager/npm/extract/pnpm.spec.ts b/lib/modules/manager/npm/extract/pnpm.spec.ts index 6174595759eb5112fa6a64a7baeb527ce1af3a63..32259488514a59fa41b4e1b2473400f1f67a23de 100644 --- a/lib/modules/manager/npm/extract/pnpm.spec.ts +++ b/lib/modules/manager/npm/extract/pnpm.spec.ts @@ -7,7 +7,6 @@ import { detectPnpmWorkspaces, extractPnpmFilters, findPnpmWorkspace, - getConstraints, getPnpmLock, } from './pnpm'; @@ -231,53 +230,6 @@ describe('modules/manager/npm/extract/pnpm', () => { }); }); - describe('getConstraints()', () => { - // no constraints - it.each([ - [6.0, undefined, '>=8'], - [5.4, undefined, '>=7 <8'], - [5.3, undefined, '>=6 <7'], - [5.2, undefined, '>=5.10.0 <6'], - [5.1, undefined, '>=3.5.0 <5.9.3'], - [5.0, undefined, '>=3 <3.5.0'], - ])('adds constraints for %f', (lockfileVersion, constraints, expected) => { - expect(getConstraints(lockfileVersion, constraints)).toBe(expected); - }); - - // constraints present - it.each([ - [6.0, '>=8.2.0', '>=8.2.0'], - [6.0, '>=7', '>=7 >=8'], - - [5.4, '^7.2.0', '^7.2.0'], - [5.4, '<7.2.0', '<7.2.0 >=7'], - [5.4, '>7.2.0', '>7.2.0 <8'], - [5.4, '>=6', '>=6 >=7 <8'], - - [5.3, '^6.0.0', '^6.0.0'], - [5.3, '<6.2.0', '<6.2.0 >=6'], - [5.3, '>6.2.0', '>6.2.0 <7'], - [5.3, '>=5', '>=5 >=6 <7'], - - [5.2, '5.10.0', '5.10.0'], - [5.2, '>5.0.0 <5.18.0', '>5.0.0 <5.18.0 >=5.10.0'], - [5.2, '>5.10.0', '>5.10.0 <6'], - [5.2, '>=5', '>=5 >=5.10.0 <6'], - - [5.1, '^4.0.0', '^4.0.0'], - [5.1, '<4', '<4 >=3.5.0'], - [5.1, '>=4', '>=4 <5.9.3'], - [5.1, '>=3', '>=3 >=3.5.0 <5.9.3'], - - [5.0, '3.1.0', '3.1.0'], - [5.0, '^3.0.0', '^3.0.0 <3.5.0'], - [5.0, '>=3', '>=3 <3.5.0'], - [5.0, '>=2', '>=2 >=3 <3.5.0'], - ])('adds constraints for %f', (lockfileVersion, constraints, expected) => { - expect(getConstraints(lockfileVersion, constraints)).toBe(expected); - }); - }); - describe('.getPnpmLock()', () => { const readLocalFile = jest.spyOn(fs, 'readLocalFile'); diff --git a/lib/modules/manager/npm/extract/pnpm.ts b/lib/modules/manager/npm/extract/pnpm.ts index 18e48b8185e1607f9d9703916193f163ddc60ab1..3ceeabc30bb001ac8cd780d6f198cf02d2881ef8 100644 --- a/lib/modules/manager/npm/extract/pnpm.ts +++ b/lib/modules/manager/npm/extract/pnpm.ts @@ -1,7 +1,6 @@ import is from '@sindresorhus/is'; import { findPackages } from 'find-packages'; import { load } from 'js-yaml'; -import semver from 'semver'; import upath from 'upath'; import { GlobalConfig } from '../../../../config/global'; import { logger } from '../../../../logger'; @@ -191,101 +190,3 @@ export async function getPnpmLock(filePath: string): Promise<LockFile> { return { lockedVersions: {} }; } } - -export function getConstraints( - lockfileVersion: number, - constraints?: string -): string { - let newConstraints = constraints; - - // find matching lockfileVersion and use its constraints - // if no match found use lockfileVersion 5 - // lockfileVersion 5 is the minimum version required to generate the pnpm-lock.yaml file - const { lowerBound, upperBound, lowerConstraint, upperConstraint } = - lockToPnpmVersionMapping.find( - (m) => m.lockfileVersion === lockfileVersion - ) ?? { - lockfileVersion: 5.0, - lowerBound: '2.24.0', - upperBound: '3.5.0', - lowerConstraint: '>=3', - upperConstraint: '<3.5.0', - }; - - // inorder to ensure that the constraint doesn't allow any pnpm versions that can't generate the extracted lockfileVersion - // compare the current constraint to the lowerBound and upperBound of the lockfileVersion - // if the current constraint is not comaptible, add the lowerConstraint and upperConstraint, whichever is needed - if (newConstraints) { - // if constraint satisfies versions lower than lowerBound add the lowerConstraint to narrow the range - if (semver.satisfies(lowerBound, newConstraints)) { - newConstraints += ` ${lowerConstraint}`; - } - - // if constraint satisfies versions higher than upperBound add the upperConstraint to narrow the range - if ( - upperBound && - upperConstraint && - semver.satisfies(upperBound, newConstraints) - ) { - newConstraints += ` ${upperConstraint}`; - } - } - // if no constraint is present, add the lowerConstraint and upperConstraint corresponding to the lockfileVersion - else { - newConstraints = `${lowerConstraint}${ - upperConstraint ? ` ${upperConstraint}` : '' - }`; - } - - return newConstraints; -} - -/** - pnpm lockfiles have corresponding version numbers called "lockfileVersion" - each lockfileVersion can only be generated by a certain pnpm version ranges - eg. lockfileVersion: 5.4 can only be generated by pnpm version >=7 && <8 - official list can be found here : https:github.com/pnpm/spec/tree/master/lockfile - we use the mapping present below to find the compatible pnpm version range for a given lockfileVersion - - the various terms used in the mapping are explained below: - lowerConstriant : lowest pnpm version that can generate the lockfileVersion - upperConstraint : highest pnpm version that can generate the lockfileVersion - lowerBound : highest pnpm version that is less than the lowerConstraint - upperBound : lowest pnpm version that is greater than upperConstraint - - For handling future lockfileVersions, we need to: - 1. add a upperBound and upperConstraint to the current lastest lockfileVersion - 2. add an object for the new lockfileVersion with lowerBound and lowerConstraint - */ - -const lockToPnpmVersionMapping = [ - { lockfileVersion: 6.0, lowerBound: '7.32.0', lowerConstraint: '>=8' }, - { - lockfileVersion: 5.4, - lowerBound: '6.35.1', - upperBound: '8.0.0', - lowerConstraint: '>=7', - upperConstraint: '<8', - }, - { - lockfileVersion: 5.3, - lowerBound: '5.18.10', - upperBound: '7.0.0', - lowerConstraint: '>=6', - upperConstraint: '<7', - }, - { - lockfileVersion: 5.2, - lowerBound: '5.9.3', - upperBound: '5.18.10', - lowerConstraint: '>=5.10.0', - upperConstraint: '<6', - }, - { - lockfileVersion: 5.1, - lowerBound: '3.4.1', - upperBound: '5.9.3', - lowerConstraint: '>=3.5.0', - upperConstraint: '<5.9.3', - }, -]; diff --git a/lib/modules/manager/npm/post-update/pnpm.spec.ts b/lib/modules/manager/npm/post-update/pnpm.spec.ts index ed632a38c128ddede8b37bda96e7337d5430439a..5acd7a58a4a63c8934fc395fa8011af5c439cb94 100644 --- a/lib/modules/manager/npm/post-update/pnpm.spec.ts +++ b/lib/modules/manager/npm/post-update/pnpm.spec.ts @@ -266,4 +266,42 @@ describe('modules/manager/npm/post-update/pnpm', () => { }, ]); }); + + describe('getConstraintsFromLockFile()', () => { + it('returns null if no lock file', async () => { + fs.readLocalFile.mockResolvedValueOnce(null); + const res = await pnpmHelper.getConstraintFromLockFile('some-file-name'); + expect(res).toBeNull(); + }); + + it('returns null when error reading lock file', async () => { + fs.readLocalFile.mockRejectedValueOnce(new Error('foo')); + const res = await pnpmHelper.getConstraintFromLockFile('some-file-name'); + expect(res).toBeNull(); + }); + + it('returns null if no lockfileVersion', async () => { + fs.readLocalFile.mockResolvedValueOnce('foo: bar\n'); + const res = await pnpmHelper.getConstraintFromLockFile('some-file-name'); + expect(res).toBeNull(); + }); + + it('returns null if lockfileVersion is not a number', async () => { + fs.readLocalFile.mockResolvedValueOnce('lockfileVersion: foo\n'); + const res = await pnpmHelper.getConstraintFromLockFile('some-file-name'); + expect(res).toBeNull(); + }); + + it('returns default if lockfileVersion is 1', async () => { + fs.readLocalFile.mockResolvedValueOnce('lockfileVersion: 1\n'); + const res = await pnpmHelper.getConstraintFromLockFile('some-file-name'); + expect(res).toBe('>=3 <3.5.0'); + }); + + it('maps supported versions', async () => { + fs.readLocalFile.mockResolvedValueOnce('lockfileVersion: 5.3\n'); + const res = await pnpmHelper.getConstraintFromLockFile('some-file-name'); + expect(res).toBe('>=6 <7'); + }); + }); }); diff --git a/lib/modules/manager/npm/post-update/pnpm.ts b/lib/modules/manager/npm/post-update/pnpm.ts index dccc3a5c9225351b067185acc1f21e8cb893a812..a5263bdacd0cba1a9f8cd52ac563689299b20590 100644 --- a/lib/modules/manager/npm/post-update/pnpm.ts +++ b/lib/modules/manager/npm/post-update/pnpm.ts @@ -41,9 +41,10 @@ export async function generateLockFile( const pnpmToolConstraint: ToolConstraint = { toolName: 'pnpm', constraint: - getPnpmConstraintFromUpgrades(upgrades) ?? - config.constraints?.pnpm ?? - (await getPnpmConstraint(lockFileDir)), + getPnpmConstraintFromUpgrades(upgrades) ?? // if pnpm is being upgraded, it comes first + config.constraints?.pnpm ?? // from user config or extraction + (await getPnpmConstraintFromPackageFile(lockFileDir)) ?? // look in package.json > packageManager or engines + (await getConstraintFromLockFile(lockFileName)), // use lockfileVersion to find pnpm version range }; const extraEnv: ExtraEnv = { @@ -116,10 +117,10 @@ export async function generateLockFile( return { lockFile }; } -async function getPnpmConstraint( +export async function getPnpmConstraintFromPackageFile( lockFileDir: string ): Promise<string | undefined> { - let result: string | undefined; + let constraint: string | undefined; const rootPackageJson = upath.join(lockFileDir, 'package.json'); const content = await readLocalFile(rootPackageJson, 'utf8'); if (content) { @@ -129,27 +130,89 @@ async function getPnpmConstraint( const nameAndVersion = packageManager.split('@'); const name = nameAndVersion[0]; if (name === 'pnpm') { - result = nameAndVersion[1]; + constraint = nameAndVersion[1]; } } else { const engines = packageJson?.engines; if (engines) { - result = engines['pnpm']; + constraint = engines['pnpm']; } } } - if (!result) { - const lockFileName = upath.join(lockFileDir, 'pnpm-lock.yaml'); - const content = await readLocalFile(lockFileName, 'utf8'); - if (content) { - const pnpmLock = load(content) as PnpmLockFile; - if ( - is.number(pnpmLock.lockfileVersion) && - pnpmLock.lockfileVersion < 5.4 - ) { - result = '<7'; - } + return constraint; +} + +export async function getConstraintFromLockFile( + lockFileName: string +): Promise<string | null> { + let constraint: string | null = null; + try { + const lockfileContent = await readLocalFile(lockFileName, 'utf8'); + if (!lockfileContent) { + return null; + } + const pnpmLock = load(lockfileContent) as PnpmLockFile; + if (!is.number(pnpmLock?.lockfileVersion)) { + return null; } + // find matching lockfileVersion and use its constraints + // if no match found use lockfileVersion 5 + // lockfileVersion 5 is the minimum version required to generate the pnpm-lock.yaml file + const { lowerConstraint, upperConstraint } = lockToPnpmVersionMapping.find( + (m) => m.lockfileVersion === pnpmLock.lockfileVersion + ) ?? { + lockfileVersion: 5.0, + lowerConstraint: '>=3', + upperConstraint: '<3.5.0', + }; + constraint = lowerConstraint; + if (upperConstraint) { + constraint += ` ${upperConstraint}`; + } + } catch (err) { + logger.warn({ err }, 'Error getting pnpm constraints from lock file'); } - return result; + return constraint; } + +/** + pnpm lockfiles have corresponding version numbers called "lockfileVersion" + each lockfileVersion can only be generated by a certain pnpm version ranges + eg. lockfileVersion: 5.4 can only be generated by pnpm version >=7 && <8 + official list can be found here : https://github.com/pnpm/spec/tree/master/lockfile + we use the mapping present below to find the compatible pnpm version range for a given lockfileVersion + + the various terms used in the mapping are explained below: + lowerConstriant : lowest pnpm version that can generate the lockfileVersion + upperConstraint : highest pnpm version that can generate the lockfileVersion + lowerBound : highest pnpm version that is less than the lowerConstraint + upperBound : lowest pnpm version that is greater than upperConstraint + + For handling future lockfileVersions, we need to: + 1. add a upperBound and upperConstraint to the current lastest lockfileVersion + 2. add an object for the new lockfileVersion with lowerBound and lowerConstraint + */ + +const lockToPnpmVersionMapping = [ + { lockfileVersion: 6.0, lowerConstraint: '>=8' }, + { + lockfileVersion: 5.4, + lowerConstraint: '>=7', + upperConstraint: '<8', + }, + { + lockfileVersion: 5.3, + lowerConstraint: '>=6', + upperConstraint: '<7', + }, + { + lockfileVersion: 5.2, + lowerConstraint: '>=5.10.0', + upperConstraint: '<6', + }, + { + lockfileVersion: 5.1, + lowerConstraint: '>=3.5.0', + upperConstraint: '<5.9.3', + }, +];