From f39d608acb42db5c0813ef39104e8fdf645a25a3 Mon Sep 17 00:00:00 2001 From: jereklas <65559301+jereklas@users.noreply.github.com> Date: Fri, 24 Feb 2023 16:32:29 -0800 Subject: [PATCH] feat: add `parent>child` selector syntax to allowedVersions for peer dependency override capability (#6108) Co-authored-by: Zoltan Kochan --- .changeset/large-turkeys-hope.md | 6 ++ config/parse-overrides/src/index.ts | 4 +- config/parse-overrides/test/index.ts | 2 +- hooks/read-package-hook/package.json | 1 + .../src/createPeerDependencyPatcher.ts | 73 +++++++++++++++++-- .../src/createVersionsOverrider.ts | 23 +++--- hooks/read-package-hook/src/isSubRange.ts | 9 +++ .../test/createPeerDependencyPatcher.test.ts | 61 ++++++++++++++++ .../test/createVersionOverrider.test.ts | 6 ++ hooks/read-package-hook/tsconfig.json | 3 + pnpm-lock.yaml | 3 + 11 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 .changeset/large-turkeys-hope.md create mode 100644 hooks/read-package-hook/src/isSubRange.ts diff --git a/.changeset/large-turkeys-hope.md b/.changeset/large-turkeys-hope.md new file mode 100644 index 0000000000..cb347dbbd8 --- /dev/null +++ b/.changeset/large-turkeys-hope.md @@ -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). diff --git a/config/parse-overrides/src/index.ts b/config/parse-overrides/src/index.ts index ec9ab659dd..14df2a7558 100644 --- a/config/parse-overrides/src/index.ts +++ b/config/parse-overrides/src/index.ts @@ -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, diff --git a/config/parse-overrides/test/index.ts b/config/parse-overrides/test/index.ts index 6a05cc1839..e075106347 100644 --- a/config/parse-overrides/test/index.ts +++ b/config/parse-overrides/test/index.ts @@ -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') }) diff --git a/hooks/read-package-hook/package.json b/hooks/read-package-hook/package.json index ecf9853db4..a59dfa96ff 100644 --- a/hooks/read-package-hook/package.json +++ b/hooks/read-package-hook/package.json @@ -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:*", diff --git a/hooks/read-package-hook/src/createPeerDependencyPatcher.ts b/hooks/read-package-hook/src/createPeerDependencyPatcher.ts index 85cfc7e5af..270ac24c83 100644 --- a/hooks/read-package-hook/src/createPeerDependencyPatcher.ts +++ b/hooks/read-package-hook/src/createPeerDependencyPatcher.ts @@ -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> & { ranges: string[] }>> + +function parseAllowedVersions (allowedVersions: Record) { + const overrides = tryParseAllowedVersions(allowedVersions) + const allowedVersionsMatchAll: Record = {} + 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): 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 { + 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) +} + +function parseVersions (versions: string): string[] { return versions.split('||').map(v => v.trim()) } diff --git a/hooks/read-package-hook/src/createVersionsOverrider.ts b/hooks/read-package-hook/src/createVersionsOverrider.ts index c8e4c088d3..d2a7c50590 100644 --- a/hooks/read-package-hook/src/createVersionsOverrider.ts +++ b/hooks/read-package-hook/src/createVersionsOverrider.ts @@ -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, 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) { + 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) -} diff --git a/hooks/read-package-hook/src/isSubRange.ts b/hooks/read-package-hook/src/isSubRange.ts new file mode 100644 index 0000000000..cbf2fcf858 --- /dev/null +++ b/hooks/read-package-hook/src/isSubRange.ts @@ -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) +} diff --git a/hooks/read-package-hook/test/createPeerDependencyPatcher.test.ts b/hooks/read-package-hook/test/createPeerDependencyPatcher.test.ts index ca1357de6a..fc2d7ff9e1 100644 --- a/hooks/read-package-hook/test/createPeerDependencyPatcher.test.ts +++ b/hooks/read-package-hook/test/createPeerDependencyPatcher.test.ts @@ -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') +}) diff --git a/hooks/read-package-hook/test/createVersionOverrider.test.ts b/hooks/read-package-hook/test/createVersionOverrider.test.ts index 882fbc9bd7..1c4aeece6d 100644 --- a/hooks/read-package-hook/test/createVersionOverrider.test.ts +++ b/hooks/read-package-hook/test/createVersionOverrider.test.ts @@ -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') +}) diff --git a/hooks/read-package-hook/tsconfig.json b/hooks/read-package-hook/tsconfig.json index ce90ab69e1..6ac1e6e1c0 100644 --- a/hooks/read-package-hook/tsconfig.json +++ b/hooks/read-package-hook/tsconfig.json @@ -15,6 +15,9 @@ { "path": "../../config/parse-overrides" }, + { + "path": "../../packages/error" + }, { "path": "../../packages/parse-wanted-dependency" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32bac48f5f..6e31170c88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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