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-`
This commit is contained in:
Khải
2025-01-07 20:55:22 +07:00
committed by GitHub
parent e050221384
commit 26fe99440d
4 changed files with 111 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/resolve-dependencies": major
"pnpm": patch
---
Refuse to install when `peerDependencies` has specifications that don't make sense.

View File

@@ -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<WantedDependency & {
@@ -30,6 +31,7 @@ export async function toResolveImporter (
},
project: ImporterToResolve
): Promise<ResolveImporter> {
validatePeerDependencies(project)
const allDeps = getWantedDependencies(project.manifest)
const nonLinkedDependencies = await partitionLinkedPackages(allDeps, {
lockfileOnly: opts.lockfileOnly,

View File

@@ -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<ProjectManifest, 'name' | 'peerDependencies'>
}
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:')
}

View File

@@ -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\'')
})