mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-28 02:53:15 -04:00
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:
6
.changeset/empty-chairs-push.md
Normal file
6
.changeset/empty-chairs-push.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/resolve-dependencies": major
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Refuse to install when `peerDependencies` has specifications that don't make sense.
|
||||
@@ -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,
|
||||
|
||||
@@ -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:')
|
||||
}
|
||||
@@ -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\'')
|
||||
})
|
||||
Reference in New Issue
Block a user