From eac7bab229fcd0b42e81e5dc46c1b8cccaf48d9a Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Thu, 7 Aug 2025 01:39:01 +0200 Subject: [PATCH] fix: validate that the version in the lockfile satisfies the range (#9832) --- .changeset/bumpy-cups-arrive.md | 5 +++ .../src/satisfiesPackageManifest.ts | 10 ++++++ .../test/allProjectsAreUpToDate.test.ts | 34 +++++++++++++++++++ .../test/satisfiesPackageManifest.ts | 16 +++++++++ 4 files changed, 65 insertions(+) create mode 100644 .changeset/bumpy-cups-arrive.md diff --git a/.changeset/bumpy-cups-arrive.md b/.changeset/bumpy-cups-arrive.md new file mode 100644 index 0000000000..84d7a8e491 --- /dev/null +++ b/.changeset/bumpy-cups-arrive.md @@ -0,0 +1,5 @@ +--- +"@pnpm/lockfile.verification": patch +--- + +satisfiesPackageManifest also checks if the version in the importer satisfied the range in the `package.json`. diff --git a/lockfile/verification/src/satisfiesPackageManifest.ts b/lockfile/verification/src/satisfiesPackageManifest.ts index f530095cc4..cb2fd6a7ef 100644 --- a/lockfile/verification/src/satisfiesPackageManifest.ts +++ b/lockfile/verification/src/satisfiesPackageManifest.ts @@ -1,3 +1,4 @@ +import * as dp from '@pnpm/dependency-path' import { type ProjectSnapshot } from '@pnpm/lockfile.types' import { DEPENDENCIES_FIELDS, @@ -6,6 +7,7 @@ import { import equals from 'ramda/src/equals' import pickBy from 'ramda/src/pickBy' import omit from 'ramda/src/omit' +import semver from 'semver' import { type Diff, diffFlatRecords, isEqual } from './diffFlatRecords' export function satisfiesPackageManifest ( @@ -95,6 +97,14 @@ export function satisfiesPackageManifest ( detailedReason: `importer ${depField}.${depName} specifier ${importer.specifiers[depName]} don't match package manifest specifier (${pkgDeps[depName]})`, } } + if (importer?.specifiers[depName] == null || !semver.validRange(importer?.specifiers[depName])) continue + const version = dp.removeSuffix(importerDeps[depName]) + if (semver.valid(version) && !semver.satisfies(version, importer.specifiers[depName])) { + return { + satisfies: false, + detailedReason: `The importer resolution is broken at dependency "${depName}": version "${version}" doesn't satisfy range "${importer.specifiers[depName]}"`, + } + } } } return { satisfies: true } diff --git a/lockfile/verification/test/allProjectsAreUpToDate.test.ts b/lockfile/verification/test/allProjectsAreUpToDate.test.ts index 588abe4382..262bfdbad2 100644 --- a/lockfile/verification/test/allProjectsAreUpToDate.test.ts +++ b/lockfile/verification/test/allProjectsAreUpToDate.test.ts @@ -866,3 +866,37 @@ test('allProjectsAreUpToDate(): returns true if one of the importers is not pres lockfileDir: '', })).toBeTruthy() }) + +test('allProjectsAreUpToDate(): returns false if the lockfile is broken, the resolved versions do not satisfy the ranges', async () => { + expect(await allProjectsAreUpToDate([ + { + id: '.' as ProjectId, + manifest: { + dependencies: { + '@apollo/client': '3.3.7', + }, + }, + rootDir: '.' as ProjectRootDir, + }, + ], { + autoInstallPeers: false, + catalogs: {}, + excludeLinksFromLockfile: false, + linkWorkspacePackages: true, + wantedLockfile: { + importers: { + ['.' as ProjectId]: { + dependencies: { + '@apollo/client': '3.13.8(@types/react@18.3.23)(graphql@15.8.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(subscriptions-transport-ws@0.11.0(graphql@15.8.0))', + }, + specifiers: { + '@apollo/client': '3.3.7', + }, + }, + }, + lockfileVersion: LOCKFILE_VERSION, + }, + workspacePackages, + lockfileDir: '', + })).toBeFalsy() +}) diff --git a/lockfile/verification/test/satisfiesPackageManifest.ts b/lockfile/verification/test/satisfiesPackageManifest.ts index 308da4c4fd..87b87bae07 100644 --- a/lockfile/verification/test/satisfiesPackageManifest.ts +++ b/lockfile/verification/test/satisfiesPackageManifest.ts @@ -378,4 +378,20 @@ test('satisfiesPackageManifest()', () => { }, } )).toStrictEqual({ satisfies: true }) + + expect(satisfiesPackageManifest({}, { + dependencies: { + '@apollo/client': '3.13.8(@types/react@18.3.23)(graphql@15.8.0)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(subscriptions-transport-ws@0.11.0(graphql@15.8.0))', + }, + specifiers: { + '@apollo/client': '3.3.7', + }, + }, { + dependencies: { + '@apollo/client': '3.3.7', + }, + })).toStrictEqual({ + satisfies: false, + detailedReason: 'The importer resolution is broken at dependency "@apollo/client": version "3.13.8" doesn\'t satisfy range "3.3.7"', + }) })