From 26fe99440d11c0cbd37fc4ab2763bf3f50ada4d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kh=E1=BA=A3i?= Date: Tue, 7 Jan 2025 20:55:22 +0700 Subject: [PATCH] feat!: prevent nonsense `peerDependencies` (#8942) * feat!: prevent nonsense `peerDependencies` close #8934 * fix: eslint * feat: refuse aliases * feat: only validate packages from the workspace * refactor: change call signature and error messages * docs(hint): improve wordings * docs: add quotes to make it more readable * test: remove `use-` --- .changeset/empty-chairs-push.md | 6 ++ .../src/toResolveImporter.ts | 2 + .../src/validatePeerDependencies.ts | 29 ++++++++ .../test/validatePeerDependencies.test.ts | 74 +++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 .changeset/empty-chairs-push.md create mode 100644 pkg-manager/resolve-dependencies/src/validatePeerDependencies.ts create mode 100644 pkg-manager/resolve-dependencies/test/validatePeerDependencies.test.ts diff --git a/.changeset/empty-chairs-push.md b/.changeset/empty-chairs-push.md new file mode 100644 index 0000000000..a8b815565b --- /dev/null +++ b/.changeset/empty-chairs-push.md @@ -0,0 +1,6 @@ +--- +"@pnpm/resolve-dependencies": major +"pnpm": patch +--- + +Refuse to install when `peerDependencies` has specifications that don't make sense. diff --git a/pkg-manager/resolve-dependencies/src/toResolveImporter.ts b/pkg-manager/resolve-dependencies/src/toResolveImporter.ts index 0fe648d037..6aeeba7ba1 100644 --- a/pkg-manager/resolve-dependencies/src/toResolveImporter.ts +++ b/pkg-manager/resolve-dependencies/src/toResolveImporter.ts @@ -10,6 +10,7 @@ import { type ImporterToResolve } from '.' import { getWantedDependencies, type WantedDependency } from './getWantedDependencies' import { type ImporterToResolveGeneric } from './resolveDependencyTree' import { safeIsInnerLink } from './safeIsInnerLink' +import { validatePeerDependencies } from './validatePeerDependencies' export interface ResolveImporter extends ImporterToResolve, ImporterToResolveGeneric<{ isNew?: boolean }> { wantedDependencies: Array { + validatePeerDependencies(project) const allDeps = getWantedDependencies(project.manifest) const nonLinkedDependencies = await partitionLinkedPackages(allDeps, { lockfileOnly: opts.lockfileOnly, diff --git a/pkg-manager/resolve-dependencies/src/validatePeerDependencies.ts b/pkg-manager/resolve-dependencies/src/validatePeerDependencies.ts new file mode 100644 index 0000000000..60acd81d71 --- /dev/null +++ b/pkg-manager/resolve-dependencies/src/validatePeerDependencies.ts @@ -0,0 +1,29 @@ +import { PnpmError } from '@pnpm/error' +import { type ProjectManifest } from '@pnpm/types' +import { validRange } from 'semver' + +export interface ProjectToValidate { + rootDir: string + manifest: Pick +} + +export function validatePeerDependencies (project: ProjectToValidate): void { + const { name, peerDependencies } = project.manifest + const projectId = name ?? project.rootDir + for (const depName in peerDependencies) { + const version = peerDependencies[depName] + if (!isValidPeerVersion(version)) { + throw new PnpmError( + 'INVALID_PEER_DEPENDENCY_SPECIFICATION', + `The peerDependencies field named '${depName}' of package '${projectId}' has an invalid value: '${version}'`, + { + hint: 'The values in peerDependencies should be either a valid semver range, a `workspace:` spec, or a `catalog:` spec', + } + ) + } + } +} + +function isValidPeerVersion (version: string): boolean { + return typeof validRange(version) === 'string' || version.startsWith('workspace:') || version.startsWith('catalog:') +} diff --git a/pkg-manager/resolve-dependencies/test/validatePeerDependencies.test.ts b/pkg-manager/resolve-dependencies/test/validatePeerDependencies.test.ts new file mode 100644 index 0000000000..e07c0725c2 --- /dev/null +++ b/pkg-manager/resolve-dependencies/test/validatePeerDependencies.test.ts @@ -0,0 +1,74 @@ +import { validatePeerDependencies } from '../src/validatePeerDependencies' + +test('accepts valid specifications that make sense for peerDependencies', () => { + validatePeerDependencies({ + rootDir: '/repo/packages/pkg', + manifest: { + peerDependencies: { + 'semver-range': '>=1.2.3 || ^3.2.1', + 'workspace-scheme': 'workspace:^', + 'catalog-scheme': 'catalog:', + }, + }, + }) +}) + +test('forbids aliases', () => { + expect(validatePeerDependencies.bind(null, { + rootDir: '/repo/packages/pkg', + manifest: { + peerDependencies: { + foo: 'bar@1.2.3', + }, + }, + })).toThrow('The peerDependencies field named \'foo\' of package \'/repo/packages/pkg\' has an invalid value: \'bar@1.2.3\'') + expect(validatePeerDependencies.bind(null, { + rootDir: '/repo/packages/pkg', + manifest: { + name: 'my-pkg', + peerDependencies: { + foo: 'bar@1.2.3', + }, + }, + })).toThrow('The peerDependencies field named \'foo\' of package \'my-pkg\' has an invalid value: \'bar@1.2.3\'') +}) + +test('forbids `file:` scheme', () => { + expect(validatePeerDependencies.bind(null, { + rootDir: '/repo/packages/pkg', + manifest: { + peerDependencies: { + foo: 'file:../foo', + }, + }, + })).toThrow('The peerDependencies field named \'foo\' of package \'/repo/packages/pkg\' has an invalid value: \'file:../foo\'') + expect(validatePeerDependencies.bind(null, { + rootDir: '/repo/packages/pkg', + manifest: { + name: 'my-pkg', + peerDependencies: { + foo: 'file:../foo', + }, + }, + })).toThrow('The peerDependencies field named \'foo\' of package \'my-pkg\' has an invalid value: \'file:../foo\'') +}) + +test('forbids `link:` scheme', () => { + expect(validatePeerDependencies.bind(null, { + rootDir: '/repo/packages/pkg', + manifest: { + peerDependencies: { + foo: 'link:../foo', + }, + }, + })).toThrow('The peerDependencies field named \'foo\' of package \'/repo/packages/pkg\' has an invalid value: \'link:../foo\'') + expect(validatePeerDependencies.bind(null, { + rootDir: '/repo/packages/pkg', + manifest: { + name: 'my-pkg', + peerDependencies: { + foo: 'link:../foo', + }, + }, + })).toThrow('The peerDependencies field named \'foo\' of package \'my-pkg\' has an invalid value: \'link:../foo\'') +})