diff --git a/.changeset/ninety-eels-begin.md b/.changeset/ninety-eels-begin.md new file mode 100644 index 00000000000..67ff403b41e --- /dev/null +++ b/.changeset/ninety-eels-begin.md @@ -0,0 +1,5 @@ +--- +"@pnpm/lockfile-utils": major +--- + +Return details about the reason why the lockfile doesn't satisfy the manifest. diff --git a/.changeset/rotten-rivers-ring.md b/.changeset/rotten-rivers-ring.md new file mode 100644 index 00000000000..5b4afb47c8a --- /dev/null +++ b/.changeset/rotten-rivers-ring.md @@ -0,0 +1,6 @@ +--- +"@pnpm/headless": patch +"pnpm": patch +--- + +When installation fails because the lockfile is not up-to-date with the `package.json` file(s), print out what are the differences [#6536](https://github.com/pnpm/pnpm/pull/6536). diff --git a/lockfile/lockfile-utils/src/satisfiesPackageManifest.ts b/lockfile/lockfile-utils/src/satisfiesPackageManifest.ts index ae289345b34..0096bbe56d7 100644 --- a/lockfile/lockfile-utils/src/satisfiesPackageManifest.ts +++ b/lockfile/lockfile-utils/src/satisfiesPackageManifest.ts @@ -14,8 +14,8 @@ export function satisfiesPackageManifest ( }, importer: ProjectSnapshot | undefined, pkg: ProjectManifest -) { - if (!importer) return false +): { satisfies: boolean, detailedReason?: string } { + if (!importer) return { satisfies: false, detailedReason: 'no importer' } let existingDeps: Record = { ...pkg.devDependencies, ...pkg.dependencies, ...pkg.optionalDependencies } if (opts?.autoInstallPeers) { pkg = { @@ -36,13 +36,24 @@ export function satisfiesPackageManifest ( existingDeps = pickNonLinkedDeps(existingDeps) specs = pickNonLinkedDeps(specs) } - if ( - !equals(existingDeps, specs) || - importer.publishDirectory !== pkg.publishConfig?.directory - ) { - return false + if (!equals(existingDeps, specs)) { + return { + satisfies: false, + detailedReason: `specifiers in the lockfile (${JSON.stringify(specs)}) don't match specs in package.json (${JSON.stringify(existingDeps)})`, + } + } + if (importer.publishDirectory !== pkg.publishConfig?.directory) { + return { + satisfies: false, + detailedReason: `"publishDirectory" in the lockfile (${importer.publishDirectory ?? 'undefined'}) doesn't match "publishConfig.directory" in package.json (${pkg.publishConfig?.directory ?? 'undefined'})`, + } + } + if (!equals(pkg.dependenciesMeta ?? {}, importer.dependenciesMeta ?? {})) { + return { + satisfies: false, + detailedReason: `importer dependencies meta (${JSON.stringify(importer.dependenciesMeta)}) doesn't match package manifest dependencies meta (${JSON.stringify(pkg.dependenciesMeta)})`, + } } - if (!equals(pkg.dependenciesMeta ?? {}, importer.dependenciesMeta ?? {})) return false for (const depField of DEPENDENCIES_FIELDS) { const importerDeps = importer[depField] ?? {} let pkgDeps: Record = pkg[depField] ?? {} @@ -69,15 +80,25 @@ export function satisfiesPackageManifest ( default: throw new Error(`Unknown dependency type "${depField as string}"`) } - if (pkgDepNames.length !== Object.keys(importerDeps).length && - pkgDepNames.length !== countOfNonLinkedDeps(importerDeps)) { - return false + if ( + pkgDepNames.length !== Object.keys(importerDeps).length && + pkgDepNames.length !== countOfNonLinkedDeps(importerDeps) + ) { + return { + satisfies: false, + detailedReason: `"${depField}" in the lockfile (${JSON.stringify(importerDeps)}) doesn't match the same field in package.json (${JSON.stringify(pkgDeps)})`, + } } for (const depName of pkgDepNames) { - if (!importerDeps[depName] || importer.specifiers?.[depName] !== pkgDeps[depName]) return false + if (!importerDeps[depName] || importer.specifiers?.[depName] !== pkgDeps[depName]) { + return { + satisfies: false, + detailedReason: `importer ${depField}.${depName} specifier ${importer.specifiers[depName]} don't match package manifest specifier (${pkgDeps[depName]})`, + } + } } } - return true + return { satisfies: true } } function countOfNonLinkedDeps (lockfileDeps: { [depName: string]: string }): number { diff --git a/lockfile/lockfile-utils/test/satisfiesPackageManifest.ts b/lockfile/lockfile-utils/test/satisfiesPackageManifest.ts index 18b841d3e9f..eab985deaec 100644 --- a/lockfile/lockfile-utils/test/satisfiesPackageManifest.ts +++ b/lockfile/lockfile-utils/test/satisfiesPackageManifest.ts @@ -16,7 +16,7 @@ test('satisfiesPackageManifest()', () => { ...DEFAULT_PKG_FIELDS, dependencies: { foo: '^1.0.0' }, } - )).toBe(true) + )).toStrictEqual({ satisfies: true }) expect(satisfiesPackageManifest( {}, { @@ -28,7 +28,7 @@ test('satisfiesPackageManifest()', () => { ...DEFAULT_PKG_FIELDS, dependencies: { foo: '^1.0.0' }, } - )).toBe(true) + )).toStrictEqual({ satisfies: true }) expect(satisfiesPackageManifest( {}, { @@ -39,7 +39,7 @@ test('satisfiesPackageManifest()', () => { ...DEFAULT_PKG_FIELDS, devDependencies: { foo: '^1.0.0' }, } - )).toBe(true) + )).toStrictEqual({ satisfies: true }) expect(satisfiesPackageManifest( {}, { @@ -50,7 +50,7 @@ test('satisfiesPackageManifest()', () => { ...DEFAULT_PKG_FIELDS, optionalDependencies: { foo: '^1.0.0' }, } - )).toBe(true) + )).toStrictEqual({ satisfies: true }) expect(satisfiesPackageManifest( {}, { @@ -61,7 +61,10 @@ test('satisfiesPackageManifest()', () => { ...DEFAULT_PKG_FIELDS, optionalDependencies: { foo: '^1.0.0' }, } - )).toBe(false) + )).toStrictEqual({ + satisfies: false, + detailedReason: '"optionalDependencies" in the lockfile ({}) doesn\'t match the same field in package.json ({"foo":"^1.0.0"})', + }) expect(satisfiesPackageManifest( {}, { @@ -72,7 +75,10 @@ test('satisfiesPackageManifest()', () => { ...DEFAULT_PKG_FIELDS, dependencies: { foo: '^1.1.0' }, } - )).toBe(false) + )).toStrictEqual({ + satisfies: false, + detailedReason: 'specifiers in the lockfile ({"foo":"^1.0.0"}) don\'t match specs in package.json ({"foo":"^1.1.0"})', + }) expect(satisfiesPackageManifest( {}, { @@ -83,7 +89,10 @@ test('satisfiesPackageManifest()', () => { ...DEFAULT_PKG_FIELDS, dependencies: { foo: '^1.0.0', bar: '2.0.0' }, } - )).toBe(false) + )).toStrictEqual({ + satisfies: false, + detailedReason: 'specifiers in the lockfile ({"foo":"^1.0.0"}) don\'t match specs in package.json ({"foo":"^1.0.0","bar":"2.0.0"})', + }) expect(satisfiesPackageManifest( {}, @@ -95,7 +104,10 @@ test('satisfiesPackageManifest()', () => { ...DEFAULT_PKG_FIELDS, dependencies: { foo: '^1.0.0', bar: '2.0.0' }, } - )).toBe(false) + )).toStrictEqual({ + satisfies: false, + detailedReason: '"dependencies" in the lockfile ({"foo":"1.0.0"}) doesn\'t match the same field in package.json ({"foo":"^1.0.0","bar":"2.0.0"})', + }) { const importer = { @@ -120,7 +132,7 @@ test('satisfiesPackageManifest()', () => { bar: '2.0.0', }, } - expect(satisfiesPackageManifest({}, importer, pkg)).toBe(true) + expect(satisfiesPackageManifest({}, importer, pkg)).toStrictEqual({ satisfies: true }) } { @@ -140,7 +152,10 @@ test('satisfiesPackageManifest()', () => { bar: '2.0.0', }, } - expect(satisfiesPackageManifest({}, importer, pkg)).toBe(false) + expect(satisfiesPackageManifest({}, importer, pkg)).toStrictEqual({ + satisfies: false, + detailedReason: 'specifiers in the lockfile ({"bar":"2.0.0","qar":"^1.0.0"}) don\'t match specs in package.json ({"bar":"2.0.0"})', + }) } { @@ -159,7 +174,10 @@ test('satisfiesPackageManifest()', () => { bar: '2.0.0', }, } - expect(satisfiesPackageManifest({}, importer, pkg)).toBe(false) + expect(satisfiesPackageManifest({}, importer, pkg)).toStrictEqual({ + satisfies: false, + detailedReason: '"dependencies" in the lockfile ({"bar":"2.0.0","qar":"1.0.0"}) doesn\'t match the same field in package.json ({"bar":"2.0.0"})', + }) } expect(satisfiesPackageManifest( @@ -172,7 +190,7 @@ test('satisfiesPackageManifest()', () => { ...DEFAULT_PKG_FIELDS, dependencies: { foo: '^1.0.0' }, } - )).toBe(true) + )).toStrictEqual({ satisfies: true }) expect(satisfiesPackageManifest( {}, @@ -181,7 +199,7 @@ test('satisfiesPackageManifest()', () => { ...DEFAULT_PKG_FIELDS, dependencies: { foo: '^1.0.0' }, } - )).toBe(false) + )).toStrictEqual({ satisfies: false, detailedReason: 'no importer' }) expect(satisfiesPackageManifest( {}, @@ -202,7 +220,7 @@ test('satisfiesPackageManifest()', () => { foo: '1.0.0', }, } - )).toBe(true) + )).toStrictEqual({ satisfies: true }) expect(satisfiesPackageManifest( {}, @@ -224,7 +242,7 @@ test('satisfiesPackageManifest()', () => { }, dependenciesMeta: {}, } - )).toBe(true) + )).toStrictEqual({ satisfies: true }) expect(satisfiesPackageManifest( { autoInstallPeers: true }, @@ -247,7 +265,7 @@ test('satisfiesPackageManifest()', () => { bar: '^1.0.0', }, } - )).toBe(true) + )).toStrictEqual({ satisfies: true }) expect(satisfiesPackageManifest( { autoInstallPeers: true }, @@ -284,7 +302,7 @@ test('satisfiesPackageManifest()', () => { qar: '^1.0.0', }, } - )).toBe(true) + )).toStrictEqual({ satisfies: true }) expect(satisfiesPackageManifest( {}, @@ -306,7 +324,7 @@ test('satisfiesPackageManifest()', () => { directory: 'dist', }, } - )).toBe(true) + )).toStrictEqual({ satisfies: true }) expect(satisfiesPackageManifest( {}, @@ -328,7 +346,10 @@ test('satisfiesPackageManifest()', () => { directory: 'lib', }, } - )).toBe(false) + )).toStrictEqual({ + satisfies: false, + detailedReason: '"publishDirectory" in the lockfile (dist) doesn\'t match "publishConfig.directory" in package.json (lib)', + }) expect(satisfiesPackageManifest( { @@ -349,5 +370,5 @@ test('satisfiesPackageManifest()', () => { bar: 'link:../bar', }, } - )).toBe(true) + )).toStrictEqual({ satisfies: true }) }) diff --git a/pkg-manager/core/src/install/allProjectsAreUpToDate.ts b/pkg-manager/core/src/install/allProjectsAreUpToDate.ts index fa22407009b..ae8fc01200e 100644 --- a/pkg-manager/core/src/install/allProjectsAreUpToDate.ts +++ b/pkg-manager/core/src/install/allProjectsAreUpToDate.ts @@ -39,7 +39,7 @@ export async function allProjectsAreUpToDate ( return pEvery(projects, (project) => { const importer = opts.wantedLockfile.importers[project.id] return !hasLocalTarballDepsInRoot(importer) && - _satisfiesPackageManifest(importer, project.manifest) && + _satisfiesPackageManifest(importer, project.manifest).satisfies && _linkedPackagesAreUpToDate({ dir: project.rootDir, manifest: project.manifest, diff --git a/pkg-manager/headless/src/index.ts b/pkg-manager/headless/src/index.ts index 7a2fe832a6b..67117392497 100644 --- a/pkg-manager/headless/src/index.ts +++ b/pkg-manager/headless/src/index.ts @@ -200,11 +200,15 @@ export async function headlessInstall (opts: HeadlessOptions): Promise