fix: don't fail with strictPeerDependencies=true if the peerDependencyRules ignore the peer issues (#9505)

close #9449
close #8859
close #7978
close #8382
This commit is contained in:
Zoltan Kochan
2025-05-09 11:38:56 +02:00
committed by GitHub
parent 36d1448c48
commit f0c3ed6781
18 changed files with 350 additions and 325 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/core": patch
"pnpm": patch
---
Installation should not exit with an error if `strictPeerDependencies` is `true` but all issues are ignored by `peerDependencyRules` [#9505](https://github.com/pnpm/pnpm/pull/9505).

View File

@@ -0,0 +1,6 @@
---
"@pnpm/render-peer-issues": major
"@pnpm/default-reporter": major
---
Remove filtering of peer dependency issues.

View File

@@ -10,7 +10,6 @@ import { reporterForClient } from './reporterForClient'
import { formatWarn } from './reporterForClient/utils/formatWarn'
import { reporterForServer } from './reporterForServer'
import { type FilterPkgsDiff } from './reporterForClient/reportSummary'
import { type PeerDependencyRules } from '@pnpm/types'
export { formatWarn }
@@ -29,7 +28,6 @@ export function initDefaultReporter (
hideProgressPrefix?: boolean
hideLifecycleOutput?: boolean
hideLifecyclePrefix?: boolean
peerDependencyRules?: PeerDependencyRules
}
context: {
argv: string[]
@@ -107,7 +105,6 @@ export function toOutput$ (
appendOnly?: boolean
logLevel?: LogLevel
outputMaxWidth?: number
peerDependencyRules?: PeerDependencyRules
streamLifecycleOutput?: boolean
aggregateOutput?: boolean
throttleProgress?: number
@@ -274,7 +271,6 @@ export function toOutput$ (
config: opts.context.config,
env: opts.context.env ?? process.env,
filterPkgsDiff: opts.filterPkgsDiff,
peerDependencyRules: opts.reportingOptions?.peerDependencyRules,
process: opts.context.process ?? process,
isRecursive: opts.context.config?.['recursive'] === true,
logLevel: opts.reportingOptions?.logLevel,

View File

@@ -4,7 +4,7 @@ import { renderDedupeCheckIssues } from '@pnpm/dedupe.issues-renderer'
import { type DedupeCheckIssues } from '@pnpm/dedupe.types'
import { type PnpmError } from '@pnpm/error'
import { renderPeerIssues } from '@pnpm/render-peer-issues'
import { type PeerDependencyRules, type PeerDependencyIssuesByProjects } from '@pnpm/types'
import { type PeerDependencyIssuesByProjects } from '@pnpm/types'
import chalk from 'chalk'
import equals from 'ramda/src/equals'
import StackTracey from 'stacktracey'
@@ -19,8 +19,8 @@ StackTracey.maxColumnWidths = {
const highlight = chalk.yellow
const colorPath = chalk.gray
export function reportError (logObj: Log, config?: Config, peerDependencyRules?: PeerDependencyRules): string | null {
const errorInfo = getErrorInfo(logObj, config, peerDependencyRules)
export function reportError (logObj: Log, config?: Config): string | null {
const errorInfo = getErrorInfo(logObj, config)
if (!errorInfo) return null
let output = formatErrorSummary(errorInfo.title, (logObj as LogObjWithPossibleError).err?.code)
if (logObj.pkgsStack != null) {
@@ -49,7 +49,7 @@ interface ErrorInfo {
body?: string
}
function getErrorInfo (logObj: Log, config?: Config, peerDependencyRules?: PeerDependencyRules): ErrorInfo | null {
function getErrorInfo (logObj: Log, config?: Config): ErrorInfo | null {
if ('err' in logObj && logObj.err) {
const err = logObj.err as (PnpmError & { stack: object })
switch (err.code) {
@@ -80,7 +80,7 @@ function getErrorInfo (logObj: Log, config?: Config, peerDependencyRules?: PeerD
case 'ERR_PNPM_UNSUPPORTED_ENGINE':
return reportEngineError(logObj as any) // eslint-disable-line @typescript-eslint/no-explicit-any
case 'ERR_PNPM_PEER_DEP_ISSUES':
return reportPeerDependencyIssuesError(err, logObj as any, peerDependencyRules) // eslint-disable-line @typescript-eslint/no-explicit-any
return reportPeerDependencyIssuesError(err, logObj as any) // eslint-disable-line @typescript-eslint/no-explicit-any
case 'ERR_PNPM_DEDUPE_CHECK_ISSUES':
return reportDedupeCheckIssuesError(err, logObj as any) // eslint-disable-line @typescript-eslint/no-explicit-any
case 'ERR_PNPM_SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER':
@@ -430,8 +430,7 @@ function hideSecureInfo (key: string, value: string): string {
function reportPeerDependencyIssuesError (
err: Error,
msg: { issuesByProjects: PeerDependencyIssuesByProjects },
peerDependencyRules?: PeerDependencyRules
msg: { issuesByProjects: PeerDependencyIssuesByProjects }
): ErrorInfo | null {
const hasMissingPeers = getHasMissingPeers(msg.issuesByProjects)
const hints: string[] = []
@@ -439,7 +438,7 @@ function reportPeerDependencyIssuesError (
hints.push('If you want peer dependencies to be automatically installed, add "auto-install-peers=true" to an .npmrc file at the root of your project.')
}
hints.push('If you don\'t want pnpm to fail on peer dependency issues, add "strict-peer-dependencies=false" to an .npmrc file at the root of your project.')
const rendered = renderPeerIssues(msg.issuesByProjects, { rules: peerDependencyRules })
const rendered = renderPeerIssues(msg.issuesByProjects)
if (!rendered) return null
return {
title: err.message,

View File

@@ -20,7 +20,6 @@ import { reportSkippedOptionalDependencies } from './reportSkippedOptionalDepend
import { reportStats } from './reportStats'
import { reportSummary, type FilterPkgsDiff } from './reportSummary'
import { reportUpdateCheck } from './reportUpdateCheck'
import { type PeerDependencyRules } from '@pnpm/types'
const PRINT_EXECUTION_TIME_IN_COMMANDS = {
install: true,
@@ -62,7 +61,6 @@ export function reporterForClient (
config?: Config
env: NodeJS.ProcessEnv
filterPkgsDiff?: FilterPkgsDiff
peerDependencyRules?: PeerDependencyRules
process: NodeJS.Process
isRecursive: boolean
logLevel?: LogLevel
@@ -92,7 +90,6 @@ export function reporterForClient (
cwd,
logLevel: opts.logLevel,
zoomOutCurrent: opts.isRecursive,
peerDependencyRules: opts.peerDependencyRules,
}
),
]
@@ -103,7 +100,7 @@ export function reporterForClient (
if (logLevelNumber >= LOG_LEVEL_NUMBER.warn) {
outputs.push(
reportPeerDependencyIssues(log$, opts.peerDependencyRules),
reportPeerDependencyIssues(log$),
reportDeprecations({
deprecation: log$.deprecation,
stage: log$.stage,

View File

@@ -7,7 +7,6 @@ import { filter, map } from 'rxjs/operators'
import { reportError } from '../reportError'
import { formatWarn } from './utils/formatWarn'
import { autozoom } from './utils/zooming'
import { type PeerDependencyRules } from '@pnpm/types'
// eslint-disable:object-literal-sort-keys
export const LOG_LEVEL_NUMBER: Record<LogLevel, number> = {
@@ -31,7 +30,6 @@ export function reportMisc (
logLevel?: LogLevel
config?: Config
zoomOutCurrent: boolean
peerDependencyRules?: PeerDependencyRules
}
): Rx.Observable<Rx.Observable<{ msg: string }>> {
const maxLogLevel = LOG_LEVEL_NUMBER[opts.logLevel ?? 'info'] ?? LOG_LEVEL_NUMBER['info']
@@ -45,7 +43,7 @@ export function reportMisc (
return reportWarning(obj)
}
case 'error': {
const errorOutput = reportError(obj, opts.config, opts.peerDependencyRules)
const errorOutput = reportError(obj, opts.config)
if (!errorOutput) return Rx.NEVER
if (obj['prefix'] && obj['prefix'] !== opts.cwd) {
return Rx.of({

View File

@@ -1,6 +1,5 @@
import { type PeerDependencyIssuesLog } from '@pnpm/core-loggers'
import { renderPeerIssues } from '@pnpm/render-peer-issues'
import { type PeerDependencyRules } from '@pnpm/types'
import * as Rx from 'rxjs'
import { map, take } from 'rxjs/operators'
import { formatWarn } from './utils/formatWarn'
@@ -8,15 +7,12 @@ import { formatWarn } from './utils/formatWarn'
export function reportPeerDependencyIssues (
log$: {
peerDependencyIssues: Rx.Observable<PeerDependencyIssuesLog>
},
peerDependencyRules?: PeerDependencyRules
}
): Rx.Observable<Rx.Observable<{ msg: string }>> {
return log$.peerDependencyIssues.pipe(
take(1),
map((log) => {
const renderedPeerIssues = renderPeerIssues(log.issuesByProjects, {
rules: peerDependencyRules,
})
const renderedPeerIssues = renderPeerIssues(log.issuesByProjects)
if (!renderedPeerIssues) {
return Rx.NEVER
}

View File

@@ -31,18 +31,14 @@
},
"dependencies": {
"@pnpm/error": "workspace:*",
"@pnpm/matcher": "workspace:*",
"@pnpm/parse-overrides": "workspace:*",
"@pnpm/types": "workspace:*",
"archy": "catalog:",
"chalk": "catalog:",
"cli-columns": "catalog:",
"semver": "catalog:"
"cli-columns": "catalog:"
},
"devDependencies": {
"@pnpm/render-peer-issues": "workspace:*",
"@types/archy": "catalog:",
"@types/semver": "catalog:"
"@types/archy": "catalog:"
},
"engines": {
"node": ">=18.12"

View File

@@ -1,36 +1,24 @@
import { PnpmError } from '@pnpm/error'
import { createMatcher } from '@pnpm/matcher'
import {
type BadPeerDependencyIssue,
type PeerDependencyIssuesByProjects,
type PeerDependencyRules,
} from '@pnpm/types'
import { parseOverrides, type VersionOverride } from '@pnpm/parse-overrides'
import archy from 'archy'
import chalk from 'chalk'
import cliColumns from 'cli-columns'
import semver from 'semver'
export function renderPeerIssues (
peerDependencyIssuesByProjects: PeerDependencyIssuesByProjects,
opts?: {
rules?: PeerDependencyRules
width?: number
}
): string {
const ignoreMissingPatterns = [...new Set(opts?.rules?.ignoreMissing ?? [])]
const ignoreMissingMatcher = createMatcher(ignoreMissingPatterns)
const allowAnyPatterns = [...new Set(opts?.rules?.allowAny ?? [])]
const allowAnyMatcher = createMatcher(allowAnyPatterns)
const { allowedVersionsMatchAll, allowedVersionsByParentPkgName } = parseAllowedVersions(opts?.rules?.allowedVersions ?? {})
const projects = {} as Record<string, PkgNode>
for (const [projectId, { bad, missing, conflicts, intersections }] of Object.entries(peerDependencyIssuesByProjects)) {
projects[projectId] = { dependencies: {}, peerIssues: [] }
for (const [peerName, issues] of Object.entries(missing)) {
if (
!conflicts.includes(peerName) &&
intersections[peerName] == null ||
ignoreMissingMatcher(peerName)
intersections[peerName] == null
) {
continue
}
@@ -39,20 +27,7 @@ export function renderPeerIssues (
}
}
for (const [peerName, issues] of Object.entries(bad)) {
if (allowAnyMatcher(peerName)) continue
for (const issue of issues) {
if (allowedVersionsMatchAll[peerName]?.some((range) => semver.satisfies(issue.foundVersion, range))) continue
const currentParentPkg = issue.parents.at(-1)
if (currentParentPkg && allowedVersionsByParentPkgName[peerName]?.[currentParentPkg.name]) {
const allowedVersionsByParent: Record<string, string[]> = {}
for (const { targetPkg, parentPkg, ranges } of allowedVersionsByParentPkgName[peerName][currentParentPkg.name]) {
if (!parentPkg.bareSpecifier || currentParentPkg.version &&
(isSubRange(parentPkg.bareSpecifier, currentParentPkg.version) || semver.satisfies(currentParentPkg.version, parentPkg.bareSpecifier))) {
allowedVersionsByParent[targetPkg.name] = ranges
}
}
if (allowedVersionsByParent[peerName]?.some((range) => semver.satisfies(issue.foundVersion, range))) continue
}
createTree(projects[projectId], issue.parents, formatUnmetPeerMessage({
peerName,
...issue,
@@ -141,59 +116,3 @@ function toArchyData (depName: string, pkgNode: PkgNode): archy.Data {
}
return result
}
type AllowedVersionsByParentPkgName = Record<string, Record<string, Array<Required<Pick<VersionOverride, 'parentPkg' | 'targetPkg'>> & { ranges: string[] }>>>
interface ParsedAllowedVersions {
allowedVersionsMatchAll: Record<string, string[]>
allowedVersionsByParentPkgName: AllowedVersionsByParentPkgName
}
function parseAllowedVersions (allowedVersions: Record<string, string>): ParsedAllowedVersions {
const overrides = tryParseAllowedVersions(allowedVersions)
const allowedVersionsMatchAll: Record<string, string[]> = {}
const allowedVersionsByParentPkgName: AllowedVersionsByParentPkgName = {}
for (const { parentPkg, targetPkg, newBareSpecifier } of overrides) {
const ranges = parseVersions(newBareSpecifier)
if (!parentPkg) {
allowedVersionsMatchAll[targetPkg.name] = ranges
continue
}
if (!allowedVersionsByParentPkgName[targetPkg.name]) {
allowedVersionsByParentPkgName[targetPkg.name] = {}
}
if (!allowedVersionsByParentPkgName[targetPkg.name][parentPkg.name]) {
allowedVersionsByParentPkgName[targetPkg.name][parentPkg.name] = []
}
allowedVersionsByParentPkgName[targetPkg.name][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 parseVersions (versions: string): string[] {
return versions.split('||').map(v => v.trim())
}
function isSubRange (superRange: string | undefined, subRange: string): boolean {
return !superRange ||
subRange === superRange ||
semver.validRange(subRange) != null &&
semver.validRange(superRange) != null &&
semver.subset(subRange, superRange)
}

View File

@@ -22,12 +22,6 @@ Peer dependencies that should be installed:
ddd@^1.0.0"
`;
exports[`renderPeerIssues() allowed versions 1`] = `
".
└─┬ aaa 1.0.0
└── ✕ unmet peer @foo/bar@^1.0.0: found 2.0.0"
`;
exports[`renderPeerIssues() format correctly the version ranges with spaces and "*" 1`] = `
".
└─┬ z 1.0.0

View File

@@ -108,185 +108,6 @@ test('renderPeerIssues()', () => {
}, { width: 500 }))).toMatchSnapshot()
})
test('renderPeerIssues() ignore missing', () => {
expect(stripAnsi(renderPeerIssues({
'.': {
missing: {
aaa: [
{
parents: [
{
name: 'xxx',
version: '1.0.0',
},
],
optional: false,
wantedRange: '>=1.0.0 <3.0.0',
},
],
'@foo/bar': [
{
parents: [
{
name: 'xxx',
version: '1.0.0',
},
],
optional: false,
wantedRange: '>=1.0.0 <3.0.0',
},
],
},
bad: {},
conflicts: [],
intersections: {
aaa: '^1.0.0',
'@foo/bar': '^1.0.0',
},
},
}, {
rules: {
ignoreMissing: ['aaa', '@foo/*'],
},
width: 500,
}))).toBe('')
})
test('renderPeerIssues() allow any version', () => {
expect(stripAnsi(renderPeerIssues({
'.': {
missing: {},
bad: {
bbb: [
{
parents: [
{
name: 'xxx',
version: '1.0.0',
},
],
foundVersion: '2.0.0',
resolvedFrom: [],
optional: false,
wantedRange: '^1.0.0',
},
],
'@foo/bar': [
{
parents: [
{
name: 'xxx',
version: '1.0.0',
},
],
foundVersion: '2.0.0',
resolvedFrom: [],
optional: false,
wantedRange: '^1.0.0',
},
],
},
conflicts: [],
intersections: {},
},
}, {
rules: {
allowAny: ['bbb', '@foo/*'],
},
width: 500,
}))).toBe('')
})
test('renderPeerIssues() allowed versions', () => {
expect(stripAnsi(renderPeerIssues({
'.': {
missing: {},
bad: {
bbb: [
{
parents: [
{
name: 'xxx',
version: '1.0.0',
},
],
foundVersion: '2.0.0',
resolvedFrom: [],
optional: false,
wantedRange: '^1.0.0',
},
],
'@foo/bar': [
{
parents: [
{
name: 'aaa',
version: '1.0.0',
},
],
foundVersion: '2.0.0',
resolvedFrom: [],
optional: false,
wantedRange: '^1.0.0',
},
{
parents: [
{
name: 'yyy',
version: '1.0.0',
},
{
name: 'xxx',
version: '1.0.0',
},
],
foundVersion: '2.0.0',
resolvedFrom: [],
optional: false,
wantedRange: '^1.0.0',
},
{
parents: [
{
name: 'ccc',
version: '3.0.0',
},
],
foundVersion: '3.0.0',
resolvedFrom: [],
optional: false,
wantedRange: '^1.0.0',
},
{
parents: [
{
name: 'ccc',
version: '2.3.6',
},
],
foundVersion: '4.0.0',
resolvedFrom: [],
optional: false,
wantedRange: '^1.0.0',
},
],
},
conflicts: [],
intersections: {},
},
}, {
rules: {
allowedVersions: {
bbb: '2',
'xxx>@foo/bar': '2',
'ccc@3>@foo/bar': '3',
'ccc@>=2.3.5 <3>@foo/bar': '4',
},
},
width: 500,
}))).toMatchSnapshot()
})
test('renderPeerIssues() optional peer dependencies are printed only if they are in conflict with non-optional peers', () => {
expect(stripAnsi(renderPeerIssues({
'.': {
@@ -402,9 +223,6 @@ test('renderPeerIssues() do not fail if the parents array is empty', () => {
},
},
}, {
rules: {
ignoreMissing: [],
},
width: 500,
})).trim()).toBe(`.
└─┬ <unknown> <unknown>

View File

@@ -9,12 +9,6 @@
"../../__typings__/**/*.d.ts"
],
"references": [
{
"path": "../../config/matcher"
},
{
"path": "../../config/parse-overrides"
},
{
"path": "../error"
},

View File

@@ -13,6 +13,7 @@ import {
type SupportedArchitectures,
type AllowedDeprecatedVersions,
type PackageExtension,
type PeerDependencyRules,
type ReadPackageHook,
type Registries,
type PrepareExecutionEnv,
@@ -111,6 +112,7 @@ export interface StrictInstallOptions {
symlink: boolean
enableModulesDir: boolean
modulesCacheMaxAge: number
peerDependencyRules: PeerDependencyRules
allowedDeprecatedVersions: AllowedDeprecatedVersions
ignorePatchFailures?: boolean
allowUnusedPatches: boolean

View File

@@ -1403,6 +1403,7 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
reportPeerDependencyIssues(peerDependencyIssuesByProjects, {
lockfileDir: opts.lockfileDir,
strictPeerDependencies: opts.strictPeerDependencies,
rules: opts.peerDependencyRules,
})
summaryLogger.debug({ prefix: opts.lockfileDir })

View File

@@ -1,30 +1,138 @@
import { PnpmError } from '@pnpm/error'
import { createMatcher } from '@pnpm/matcher'
import { peerDependencyIssuesLogger } from '@pnpm/core-loggers'
import { type PeerDependencyIssuesByProjects } from '@pnpm/types'
import { type PeerDependencyIssuesByProjects, type PeerDependencyRules, type BadPeerDependencyIssue } from '@pnpm/types'
import semver from 'semver'
import isEmpty from 'ramda/src/isEmpty'
import { parseOverrides, type VersionOverride } from '@pnpm/parse-overrides'
export function reportPeerDependencyIssues (
peerDependencyIssuesByProjects: PeerDependencyIssuesByProjects,
opts: {
lockfileDir: string
rules?: PeerDependencyRules
strictPeerDependencies: boolean
}
): void {
const newPeerDependencyIssuesByProjects = filterPeerDependencyIssues(peerDependencyIssuesByProjects, opts.rules)
if (
Object.values(peerDependencyIssuesByProjects).every((peerIssuesOfProject) =>
Object.values(newPeerDependencyIssuesByProjects).every((peerIssuesOfProject) =>
isEmpty(peerIssuesOfProject.bad) && (
isEmpty(peerIssuesOfProject.missing) ||
peerIssuesOfProject.conflicts.length === 0 && Object.keys(peerIssuesOfProject.intersections).length === 0
))
) return
if (opts.strictPeerDependencies) {
throw new PeerDependencyIssuesError(peerDependencyIssuesByProjects)
throw new PeerDependencyIssuesError(newPeerDependencyIssuesByProjects)
}
peerDependencyIssuesLogger.debug({
issuesByProjects: peerDependencyIssuesByProjects,
issuesByProjects: newPeerDependencyIssuesByProjects,
})
}
export function filterPeerDependencyIssues (
peerDependencyIssuesByProjects: PeerDependencyIssuesByProjects,
rules?: PeerDependencyRules
): PeerDependencyIssuesByProjects {
if (!rules) return peerDependencyIssuesByProjects
const ignoreMissingPatterns = [...new Set(rules?.ignoreMissing ?? [])]
const ignoreMissingMatcher = createMatcher(ignoreMissingPatterns)
const allowAnyPatterns = [...new Set(rules?.allowAny ?? [])]
const allowAnyMatcher = createMatcher(allowAnyPatterns)
const { allowedVersionsMatchAll, allowedVersionsByParentPkgName } = parseAllowedVersions(rules?.allowedVersions ?? {})
const newPeerDependencyIssuesByProjects: PeerDependencyIssuesByProjects = {}
for (const [projectId, { bad, missing, conflicts, intersections }] of Object.entries(peerDependencyIssuesByProjects)) {
newPeerDependencyIssuesByProjects[projectId] = { bad: {}, missing: {}, conflicts, intersections }
for (const [peerName, issues] of Object.entries(missing)) {
if (
ignoreMissingMatcher(peerName)
) {
continue
}
newPeerDependencyIssuesByProjects[projectId].missing[peerName] = issues
}
for (const [peerName, issues] of Object.entries(bad)) {
if (allowAnyMatcher(peerName)) continue
const filteredIssues: BadPeerDependencyIssue[] = []
for (const issue of issues) {
if (allowedVersionsMatchAll[peerName]?.some((range) => semver.satisfies(issue.foundVersion, range))) continue
const currentParentPkg = issue.parents.at(-1)
if (currentParentPkg && allowedVersionsByParentPkgName[peerName]?.[currentParentPkg.name]) {
const allowedVersionsByParent: Record<string, string[]> = {}
for (const { targetPkg, parentPkg, ranges } of allowedVersionsByParentPkgName[peerName][currentParentPkg.name]) {
if (!parentPkg.bareSpecifier || currentParentPkg.version &&
(isSubRange(parentPkg.bareSpecifier, currentParentPkg.version) || semver.satisfies(currentParentPkg.version, parentPkg.bareSpecifier))) {
allowedVersionsByParent[targetPkg.name] = ranges
}
}
if (allowedVersionsByParent[peerName]?.some((range) => semver.satisfies(issue.foundVersion, range))) continue
}
filteredIssues.push(issue)
}
if (filteredIssues.length) {
newPeerDependencyIssuesByProjects[projectId].bad[peerName] = filteredIssues
}
}
}
return newPeerDependencyIssuesByProjects
}
function isSubRange (superRange: string | undefined, subRange: string): boolean {
return !superRange ||
subRange === superRange ||
semver.validRange(subRange) != null &&
semver.validRange(superRange) != null &&
semver.subset(subRange, superRange)
}
type AllowedVersionsByParentPkgName = Record<string, Record<string, Array<Required<Pick<VersionOverride, 'parentPkg' | 'targetPkg'>> & { ranges: string[] }>>>
interface ParsedAllowedVersions {
allowedVersionsMatchAll: Record<string, string[]>
allowedVersionsByParentPkgName: 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 parseAllowedVersions (allowedVersions: Record<string, string>): ParsedAllowedVersions {
const overrides = tryParseAllowedVersions(allowedVersions)
const allowedVersionsMatchAll: Record<string, string[]> = {}
const allowedVersionsByParentPkgName: AllowedVersionsByParentPkgName = {}
for (const { parentPkg, targetPkg, newBareSpecifier } of overrides) {
const ranges = parseVersions(newBareSpecifier)
if (!parentPkg) {
allowedVersionsMatchAll[targetPkg.name] = ranges
continue
}
if (!allowedVersionsByParentPkgName[targetPkg.name]) {
allowedVersionsByParentPkgName[targetPkg.name] = {}
}
if (!allowedVersionsByParentPkgName[targetPkg.name][parentPkg.name]) {
allowedVersionsByParentPkgName[targetPkg.name][parentPkg.name] = []
}
allowedVersionsByParentPkgName[targetPkg.name][parentPkg.name].push({
parentPkg,
targetPkg,
ranges,
})
}
return {
allowedVersionsMatchAll,
allowedVersionsByParentPkgName,
}
}
function parseVersions (versions: string): string[] {
return versions.split('||').map(v => v.trim())
}
export class PeerDependencyIssuesError extends PnpmError {
issuesByProjects: PeerDependencyIssuesByProjects
constructor (issues: PeerDependencyIssuesByProjects) {

View File

@@ -0,0 +1,209 @@
import { filterPeerDependencyIssues } from '../src/install/reportPeerDependencyIssues'
test('filterPeerDependencyIssues() ignore missing', () => {
expect(filterPeerDependencyIssues({
'.': {
missing: {
aaa: [
{
parents: [
{
name: 'xxx',
version: '1.0.0',
}],
optional: false,
wantedRange: '>=1.0.0 <3.0.0',
}],
'@foo/bar': [
{
parents: [
{
name: 'xxx',
version: '1.0.0',
}],
optional: false,
wantedRange: '>=1.0.0 <3.0.0',
}],
},
bad: {},
conflicts: [],
intersections: {
aaa: '^1.0.0',
'@foo/bar': '^1.0.0',
},
},
}, {
ignoreMissing: ['aaa', '@foo/*'],
})).toStrictEqual({
'.': {
bad: {},
conflicts: [],
intersections: {
'@foo/bar': '^1.0.0',
aaa: '^1.0.0',
},
missing: {},
},
})
})
test('filterPeerDependencyIssues() allow any version', () => {
expect(filterPeerDependencyIssues({
'.': {
missing: {},
bad: {
bbb: [
{
parents: [
{
name: 'xxx',
version: '1.0.0',
}],
foundVersion: '2.0.0',
resolvedFrom: [],
optional: false,
wantedRange: '^1.0.0',
}],
'@foo/bar': [
{
parents: [
{
name: 'xxx',
version: '1.0.0',
}],
foundVersion: '2.0.0',
resolvedFrom: [],
optional: false,
wantedRange: '^1.0.0',
}],
},
conflicts: [],
intersections: {},
},
}, {
allowAny: ['bbb', '@foo/*'],
})).toStrictEqual({
'.': {
bad: {},
conflicts: [],
intersections: {},
missing: {},
},
})
})
test('filterPeerDependencyIssues() allowed versions', () => {
expect(filterPeerDependencyIssues({
'.': {
missing: {},
bad: {
bbb: [
{
parents: [
{
name: 'xxx',
version: '1.0.0',
},
],
foundVersion: '2.0.0',
resolvedFrom: [],
optional: false,
wantedRange: '^1.0.0',
},
],
'@foo/bar': [
{
parents: [
{
name: 'aaa',
version: '1.0.0',
},
],
foundVersion: '2.0.0',
resolvedFrom: [],
optional: false,
wantedRange: '^1.0.0',
},
{
parents: [
{
name: 'yyy',
version: '1.0.0',
},
{
name: 'xxx',
version: '1.0.0',
},
],
foundVersion: '2.0.0',
resolvedFrom: [],
optional: false,
wantedRange: '^1.0.0',
},
{
parents: [
{
name: 'ccc',
version: '3.0.0',
},
],
foundVersion: '3.0.0',
resolvedFrom: [],
optional: false,
wantedRange: '^1.0.0',
},
{
parents: [
{
name: 'ccc',
version: '2.3.6',
},
],
foundVersion: '4.0.0',
resolvedFrom: [],
optional: false,
wantedRange: '^1.0.0',
},
],
},
conflicts: [],
intersections: {},
},
}, {
allowedVersions: {
bbb: '2',
'xxx>@foo/bar': '2',
'ccc@3>@foo/bar': '3',
'ccc@>=2.3.5 <3>@foo/bar': '4',
},
})).toStrictEqual({
'.': {
bad: {
'@foo/bar': [
{
foundVersion: '2.0.0',
optional: false,
parents: [
{
name: 'aaa',
version: '1.0.0',
},
],
resolvedFrom: [],
wantedRange: '^1.0.0',
},
],
},
conflicts: [],
intersections: {},
missing: {},
},
})
})

12
pnpm-lock.yaml generated
View File

@@ -4224,12 +4224,6 @@ importers:
'@pnpm/error':
specifier: workspace:*
version: link:../error
'@pnpm/matcher':
specifier: workspace:*
version: link:../../config/matcher
'@pnpm/parse-overrides':
specifier: workspace:*
version: link:../../config/parse-overrides
'@pnpm/types':
specifier: workspace:*
version: link:../types
@@ -4242,9 +4236,6 @@ importers:
cli-columns:
specifier: 'catalog:'
version: 4.0.0
semver:
specifier: 'catalog:'
version: 7.7.1
devDependencies:
'@pnpm/render-peer-issues':
specifier: workspace:*
@@ -4252,9 +4243,6 @@ importers:
'@types/archy':
specifier: 'catalog:'
version: 0.0.33
'@types/semver':
specifier: 'catalog:'
version: 7.5.3
packages/types:
devDependencies:

View File

@@ -28,7 +28,6 @@ export function initReporter (
throttleProgress: 200,
hideAddedPkgsProgress: opts.config.lockfileOnly,
hideLifecyclePrefix: opts.config.reporterHidePrefix,
peerDependencyRules: opts.config.peerDependencyRules,
},
streamParser: streamParser as StreamParser<Log>,
})
@@ -46,7 +45,6 @@ export function initReporter (
logLevel: opts.config.loglevel as LogLevel,
throttleProgress: 1000,
hideLifecyclePrefix: opts.config.reporterHidePrefix,
peerDependencyRules: opts.config.peerDependencyRules,
},
streamParser: streamParser as StreamParser<Log>,
})