diff --git a/.changeset/quiet-brooms-serve.md b/.changeset/quiet-brooms-serve.md new file mode 100644 index 0000000000..1658b4f378 --- /dev/null +++ b/.changeset/quiet-brooms-serve.md @@ -0,0 +1,6 @@ +--- +"@pnpm/core": patch +"pnpm": patch +--- + +Sort keys in `packageExtensions` before calculating `packageExtensionsChecksum`. Fix [#6824](https://github.com/pnpm/pnpm/issues/6824). diff --git a/pkg-manager/core/package.json b/pkg-manager/core/package.json index 5f97a86331..fafc0c4b72 100644 --- a/pkg-manager/core/package.json +++ b/pkg-manager/core/package.json @@ -67,6 +67,7 @@ "ramda": "npm:@pnpm/ramda@0.28.1", "run-groups": "^3.0.1", "semver": "^7.5.4", + "sort-keys": "^4.2.0", "version-selector-type": "^3.0.0" }, "devDependencies": { diff --git a/pkg-manager/core/src/install/index.test.ts b/pkg-manager/core/src/install/index.test.ts new file mode 100644 index 0000000000..16b7f7afd5 --- /dev/null +++ b/pkg-manager/core/src/install/index.test.ts @@ -0,0 +1,60 @@ +import { createObjectChecksum } from './index' + +function assets () { + const sorted = { + abc: { + a: 0, + b: [0, 1, 2], + c: null, + }, + def: { + foo: 'bar', + hello: 'world', + }, + } as const + + const unsorted1 = { + abc: { + b: [0, 1, 2], + a: 0, + c: null, + }, + def: { + hello: 'world', + foo: 'bar', + }, + } as const + + const unsorted2 = { + def: { + foo: 'bar', + hello: 'world', + }, + abc: { + a: 0, + b: [0, 1, 2], + c: null, + }, + } as const + + const unsorted3 = { + def: { + hello: 'world', + foo: 'bar', + }, + abc: { + b: [0, 1, 2], + a: 0, + c: null, + }, + } as const + + return { sorted, unsorted1, unsorted2, unsorted3 } as const +} + +test('createObjectChecksum', () => { + const { sorted, unsorted1, unsorted2, unsorted3 } = assets() + expect(createObjectChecksum(unsorted1)).toBe(createObjectChecksum(sorted)) + expect(createObjectChecksum(unsorted2)).toBe(createObjectChecksum(sorted)) + expect(createObjectChecksum(unsorted3)).toBe(createObjectChecksum(sorted)) +}) diff --git a/pkg-manager/core/src/install/index.ts b/pkg-manager/core/src/install/index.ts index a5a76cf044..d0a3055eb0 100644 --- a/pkg-manager/core/src/install/index.ts +++ b/pkg-manager/core/src/install/index.ts @@ -72,6 +72,7 @@ import pickBy from 'ramda/src/pickBy' import pipeWith from 'ramda/src/pipeWith' import props from 'ramda/src/props' import unnest from 'ramda/src/unnest' +import sortKeys from 'sort-keys' import { parseWantedDependencies } from '../parseWantedDependencies' import { removeDeps } from '../uninstall/removeDeps' import { allProjectsAreUpToDate } from './allProjectsAreUpToDate' @@ -749,8 +750,8 @@ function getOutdatedLockfileSetting ( return null } -export function createObjectChecksum (obj: unknown) { - const s = JSON.stringify(obj) +export function createObjectChecksum (obj: Record) { + const s = JSON.stringify(sortKeys(obj, { deep: true })) return crypto.createHash('md5').update(s).digest('hex') } diff --git a/pkg-manager/core/test/install/packageExtensions.ts b/pkg-manager/core/test/install/packageExtensions.ts index ddd58872c3..345085b9cf 100644 --- a/pkg-manager/core/test/install/packageExtensions.ts +++ b/pkg-manager/core/test/install/packageExtensions.ts @@ -1,7 +1,7 @@ import { PnpmError } from '@pnpm/error' import { prepareEmpty } from '@pnpm/prepare' -import { addDependenciesToPackage, mutateModulesInSingleProject } from '@pnpm/core' -import { type PackageExtension } from '@pnpm/types' +import { addDependenciesToPackage, mutateModulesInSingleProject, install } from '@pnpm/core' +import { type PackageExtension, type ProjectManifest } from '@pnpm/types' import { createObjectChecksum } from '../../lib/install/index' import { testDefaults, @@ -94,6 +94,58 @@ test('manifests are extended with fields specified by packageExtensions', async ) }) +test('packageExtensionsChecksum does not change regardless of keys order', async () => { + const project = prepareEmpty() + + const packageExtensions1: Record = { + 'is-odd': { + peerDependencies: { + 'is-number': '*', + }, + }, + 'is-even': { + peerDependencies: { + 'is-number': '*', + }, + }, + } + + const packageExtensions2: Record = { + 'is-even': { + peerDependencies: { + 'is-number': '*', + }, + }, + 'is-odd': { + peerDependencies: { + 'is-number': '*', + }, + }, + } + + const manifest = (): ProjectManifest => ({ + dependencies: { + 'is-even': '*', + 'is-odd': '*', + }, + }) + + await install(manifest(), await testDefaults({ + packageExtensions: packageExtensions1, + })) + const lockfile1 = await project.readLockfile() + const checksum1 = lockfile1.packageExtensionsChecksum + + await install(manifest(), await testDefaults({ + packageExtensions: packageExtensions2, + })) + const lockfile2 = await project.readLockfile() + const checksum2 = lockfile2.packageExtensionsChecksum + + expect(checksum1).toBe(checksum2) + expect(checksum1).not.toBeFalsy() +}) + test('manifests are patched by extensions from the compatibility database', async () => { const project = prepareEmpty() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99c4eb6087..0fea7e747f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3000,6 +3000,9 @@ importers: semver: specifier: ^7.5.4 version: 7.5.4 + sort-keys: + specifier: ^4.2.0 + version: 4.2.0 version-selector-type: specifier: ^3.0.0 version: 3.0.0