mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-24 07:38:12 -05:00
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:
6
.changeset/long-facts-learn.md
Normal file
6
.changeset/long-facts-learn.md
Normal 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).
|
||||
6
.changeset/three-women-sniff.md
Normal file
6
.changeset/three-women-sniff.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/render-peer-issues": major
|
||||
"@pnpm/default-reporter": major
|
||||
---
|
||||
|
||||
Remove filtering of peer dependency issues.
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -9,12 +9,6 @@
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../config/matcher"
|
||||
},
|
||||
{
|
||||
"path": "../../config/parse-overrides"
|
||||
},
|
||||
{
|
||||
"path": "../error"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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) {
|
||||
|
||||
209
pkg-manager/core/test/filterPeerDependencyIssues.test.ts
Normal file
209
pkg-manager/core/test/filterPeerDependencyIssues.test.ts
Normal 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
12
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
@@ -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>,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user