mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-29 11:34:18 -04:00
feat: add parent>child selector syntax to allowedVersions for peer dependency override capability (#6108)
Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
6
.changeset/large-turkeys-hope.md
Normal file
6
.changeset/large-turkeys-hope.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/hooks.read-package-hook": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Extends the `pnpm.peerDependencyRules.allowedVersions` `package.json` option to support the `parent>child` selector syntax. This syntax allows for extending specific `peerDependencies` [#6108](https://github.com/pnpm/pnpm/pull/6108).
|
||||
@@ -3,7 +3,7 @@ import { parseWantedDependency } from '@pnpm/parse-wanted-dependency'
|
||||
|
||||
const DELIMITER_REGEX = /[^ |@]>/
|
||||
|
||||
interface VersionOverride {
|
||||
export interface VersionOverride {
|
||||
parentPkg?: {
|
||||
name: string
|
||||
pref?: string
|
||||
@@ -41,7 +41,7 @@ export function parseOverrides (
|
||||
function parsePkgSelector (selector: string) {
|
||||
const wantedDep = parseWantedDependency(selector)
|
||||
if (!wantedDep.alias) {
|
||||
throw new PnpmError('INVALID_OVERRIDE_SELECTOR', `Cannot parse the "${selector}" selector in the overrides`)
|
||||
throw new PnpmError('INVALID_SELECTOR', `Cannot parse the "${selector}" selector`)
|
||||
}
|
||||
return {
|
||||
name: wantedDep.alias,
|
||||
|
||||
@@ -48,5 +48,5 @@ test.each([
|
||||
})
|
||||
|
||||
test('parseOverrides() throws an exception on invalid selector', () => {
|
||||
expect(() => parseOverrides({ '%': '2' })).toThrow('Cannot parse the "%" selector in the overrides')
|
||||
expect(() => parseOverrides({ '%': '2' })).toThrow('Cannot parse the "%" selector')
|
||||
})
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/main/hooks/read-package-hook#readme",
|
||||
"dependencies": {
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/matcher": "workspace:*",
|
||||
"@pnpm/parse-overrides": "workspace:*",
|
||||
"@pnpm/parse-wanted-dependency": "workspace:*",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { PeerDependencyRules, ReadPackageHook } from '@pnpm/types'
|
||||
import { createMatcher } from '@pnpm/matcher'
|
||||
import semver from 'semver'
|
||||
import isEmpty from 'ramda/src/isEmpty'
|
||||
import { PeerDependencyRules, ReadPackageHook, PackageManifest, ProjectManifest } from '@pnpm/types'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { parseOverrides, VersionOverride } from '@pnpm/parse-overrides'
|
||||
import { createMatcher } from '@pnpm/matcher'
|
||||
import { isSubRange } from './isSubRange'
|
||||
|
||||
export function createPeerDependencyPatcher (
|
||||
peerDependencyRules: PeerDependencyRules
|
||||
@@ -9,8 +13,15 @@ export function createPeerDependencyPatcher (
|
||||
const ignoreMissingMatcher = createMatcher(ignoreMissingPatterns)
|
||||
const allowAnyPatterns = [...new Set(peerDependencyRules.allowAny ?? [])]
|
||||
const allowAnyMatcher = createMatcher(allowAnyPatterns)
|
||||
const { allowedVersionsMatchAll, allowedVersionsByParentPkgName } = parseAllowedVersions(peerDependencyRules.allowedVersions ?? {})
|
||||
const _getAllowedVersionsByParentPkg = getAllowedVersionsByParentPkg.bind(null, allowedVersionsByParentPkgName)
|
||||
|
||||
return ((pkg) => {
|
||||
if (isEmpty(pkg.peerDependencies)) return pkg
|
||||
const allowedVersions = {
|
||||
...allowedVersionsMatchAll,
|
||||
..._getAllowedVersionsByParentPkg(pkg),
|
||||
}
|
||||
for (const [peerName, peerVersion] of Object.entries(pkg.peerDependencies ?? {})) {
|
||||
if (
|
||||
ignoreMissingMatcher(peerName) &&
|
||||
@@ -33,10 +44,9 @@ export function createPeerDependencyPatcher (
|
||||
pkg.peerDependencies![peerName] = '*'
|
||||
continue
|
||||
}
|
||||
const allowedVersions = parseVersions(peerDependencyRules.allowedVersions[peerName])
|
||||
const currentVersions = parseVersions(pkg.peerDependencies![peerName])
|
||||
|
||||
allowedVersions.forEach(allowedVersion => {
|
||||
allowedVersions[peerName].forEach(allowedVersion => {
|
||||
if (!currentVersions.includes(allowedVersion)) {
|
||||
currentVersions.push(allowedVersion)
|
||||
}
|
||||
@@ -48,6 +58,59 @@ export function createPeerDependencyPatcher (
|
||||
}) as ReadPackageHook
|
||||
}
|
||||
|
||||
function parseVersions (versions: string) {
|
||||
type AllowedVersionsByParentPkgName = Record<string, Array<Required<Pick<VersionOverride, 'parentPkg' | 'targetPkg'>> & { ranges: string[] }>>
|
||||
|
||||
function parseAllowedVersions (allowedVersions: Record<string, string>) {
|
||||
const overrides = tryParseAllowedVersions(allowedVersions)
|
||||
const allowedVersionsMatchAll: Record<string, string[]> = {}
|
||||
const allowedVersionsByParentPkgName: AllowedVersionsByParentPkgName = {}
|
||||
for (const { parentPkg, targetPkg, newPref } of overrides) {
|
||||
const ranges = parseVersions(newPref)
|
||||
if (!parentPkg) {
|
||||
allowedVersionsMatchAll[targetPkg.name] = ranges
|
||||
continue
|
||||
}
|
||||
if (!allowedVersionsByParentPkgName[parentPkg.name]) {
|
||||
allowedVersionsByParentPkgName[parentPkg.name] = []
|
||||
}
|
||||
allowedVersionsByParentPkgName[parentPkg.name].push({
|
||||
parentPkg,
|
||||
targetPkg,
|
||||
ranges,
|
||||
})
|
||||
}
|
||||
return {
|
||||
allowedVersionsMatchAll,
|
||||
allowedVersionsByParentPkgName,
|
||||
}
|
||||
}
|
||||
|
||||
function tryParseAllowedVersions (allowedVersions: Record<string, string>): VersionOverride[] {
|
||||
try {
|
||||
return parseOverrides(allowedVersions ?? {})
|
||||
} catch (err) {
|
||||
throw new PnpmError('INVALID_ALLOWED_VERSION_SELECTOR',
|
||||
`${(err as PnpmError).message} in pnpm.peerDependencyRules.allowedVersions`)
|
||||
}
|
||||
}
|
||||
|
||||
function getAllowedVersionsByParentPkg (
|
||||
allowedVersionsByParentPkgName: AllowedVersionsByParentPkgName,
|
||||
pkg: PackageManifest | ProjectManifest
|
||||
): Record<string, string[]> {
|
||||
if (!pkg.name || !allowedVersionsByParentPkgName[pkg.name]) return {}
|
||||
|
||||
return allowedVersionsByParentPkgName[pkg.name]
|
||||
.reduce((acc, { targetPkg, parentPkg, ranges }) => {
|
||||
if (!pkg.peerDependencies![targetPkg.name]) return acc
|
||||
if (!parentPkg.pref || pkg.version &&
|
||||
(isSubRange(parentPkg.pref, pkg.version) || semver.satisfies(pkg.version, parentPkg.pref))) {
|
||||
acc[targetPkg.name] = ranges
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, string[]>)
|
||||
}
|
||||
|
||||
function parseVersions (versions: string): string[] {
|
||||
return versions.split('||').map(v => v.trim())
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import path from 'path'
|
||||
import semver from 'semver'
|
||||
import partition from 'ramda/src/partition'
|
||||
import { Dependencies, PackageManifest, ReadPackageHook } from '@pnpm/types'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { parseOverrides } from '@pnpm/parse-overrides'
|
||||
import normalizePath from 'normalize-path'
|
||||
import semver from 'semver'
|
||||
import { isSubRange } from './isSubRange'
|
||||
|
||||
export function createVersionsOverrider (
|
||||
overrides: Record<string, string>,
|
||||
rootDir: string
|
||||
): ReadPackageHook {
|
||||
const parsedOverrides = tryParseOverrides(overrides)
|
||||
const [versionOverrides, genericVersionOverrides] = partition(({ parentPkg }) => parentPkg != null,
|
||||
parseOverrides(overrides)
|
||||
parsedOverrides
|
||||
.map((override) => {
|
||||
let linkTarget: string | undefined
|
||||
if (override.newPref.startsWith('link:')) {
|
||||
@@ -39,6 +42,14 @@ export function createVersionsOverrider (
|
||||
}) as ReadPackageHook
|
||||
}
|
||||
|
||||
function tryParseOverrides (overrides: Record<string, string>) {
|
||||
try {
|
||||
return parseOverrides(overrides)
|
||||
} catch (e) {
|
||||
throw new PnpmError('INVALID_OVERRIDES_SELECTOR', `${(e as PnpmError).message} in pnpm.overrides`)
|
||||
}
|
||||
}
|
||||
|
||||
interface VersionOverride {
|
||||
parentPkg?: {
|
||||
name: string
|
||||
@@ -86,11 +97,3 @@ function overrideDeps (versionOverrides: VersionOverride[], deps: Dependencies,
|
||||
deps[versionOverride.targetPkg.name] = versionOverride.newPref
|
||||
}
|
||||
}
|
||||
|
||||
function isSubRange (superRange: string | undefined, subRange: string) {
|
||||
return !superRange ||
|
||||
subRange === superRange ||
|
||||
semver.validRange(subRange) != null &&
|
||||
semver.validRange(superRange) != null &&
|
||||
semver.subset(subRange, superRange)
|
||||
}
|
||||
|
||||
9
hooks/read-package-hook/src/isSubRange.ts
Normal file
9
hooks/read-package-hook/src/isSubRange.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import semver from 'semver'
|
||||
|
||||
export function isSubRange (superRange: string | undefined, subRange: string) {
|
||||
return !superRange ||
|
||||
subRange === superRange ||
|
||||
semver.validRange(subRange) != null &&
|
||||
semver.validRange(superRange) != null &&
|
||||
semver.subset(subRange, superRange)
|
||||
}
|
||||
@@ -114,3 +114,64 @@ test('createPeerDependencyPatcher() does not create duplicate extended ranges',
|
||||
nopadding: '15.0.1 || 16 || ^17.0.1 || 18.x',
|
||||
})
|
||||
})
|
||||
|
||||
test('createPeerDependencyPatcher() overrides peerDependencies when parent>child selector is used', () => {
|
||||
const patcher = createPeerDependencyPatcher({
|
||||
allowedVersions: {
|
||||
bar: '2',
|
||||
'foo>bar': '1',
|
||||
'foo@2>bar': '2 || 3',
|
||||
'foo@>=2.3.5 <3>bar': '4',
|
||||
},
|
||||
})
|
||||
let patchedPkg = patcher({
|
||||
name: 'foo',
|
||||
peerDependencies: {
|
||||
bar: '0 || 1',
|
||||
},
|
||||
}) as ProjectManifest
|
||||
expect(patchedPkg.peerDependencies).toStrictEqual({
|
||||
bar: '0 || 1',
|
||||
})
|
||||
|
||||
patchedPkg = patcher({
|
||||
name: 'foo',
|
||||
version: '2',
|
||||
peerDependencies: {
|
||||
bar: '0 || 1',
|
||||
},
|
||||
}) as ProjectManifest
|
||||
expect(patchedPkg.peerDependencies).toStrictEqual({
|
||||
bar: '0 || 1 || 2 || 3',
|
||||
})
|
||||
|
||||
patchedPkg = patcher({
|
||||
name: 'foo',
|
||||
version: '3',
|
||||
peerDependencies: {
|
||||
bar: '0 || 1',
|
||||
},
|
||||
}) as ProjectManifest
|
||||
expect(patchedPkg.peerDependencies).toStrictEqual({
|
||||
bar: '0 || 1',
|
||||
})
|
||||
|
||||
patchedPkg = patcher({
|
||||
name: 'foo',
|
||||
version: '2.3.5',
|
||||
peerDependencies: {
|
||||
bar: '0 || 1',
|
||||
},
|
||||
}) as ProjectManifest
|
||||
expect(patchedPkg.peerDependencies).toStrictEqual({
|
||||
bar: '0 || 1 || 4',
|
||||
})
|
||||
})
|
||||
|
||||
test('createPeerDependencyPathcer() throws expected error if parent>child selector cannot parse', () => {
|
||||
expect(() => createPeerDependencyPatcher({
|
||||
allowedVersions: {
|
||||
'foo > bar': '2',
|
||||
},
|
||||
})).toThrowError('Cannot parse the "foo > bar" selector in pnpm.peerDependencyRules.allowedVersions')
|
||||
})
|
||||
|
||||
@@ -291,3 +291,9 @@ test('createVersionsOverrider() overrides dependencies with file specified with
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('createVersionOverrider() throws error when supplied an invalid selector', () => {
|
||||
expect(() => createVersionsOverrider({
|
||||
'foo > bar': '2',
|
||||
}, process.cwd())).toThrowError('Cannot parse the "foo > bar" selector in pnpm.overrides')
|
||||
})
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
{
|
||||
"path": "../../config/parse-overrides"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/error"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/parse-wanted-dependency"
|
||||
},
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -1609,6 +1609,9 @@ importers:
|
||||
|
||||
hooks/read-package-hook:
|
||||
dependencies:
|
||||
'@pnpm/error':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/error
|
||||
'@pnpm/matcher':
|
||||
specifier: workspace:*
|
||||
version: link:../../config/matcher
|
||||
|
||||
Reference in New Issue
Block a user