mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-08 16:07:03 -04:00
feat: detect conflicting peer dependencies per project (#4111)
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
"@pnpm/core": minor
|
||||
---
|
||||
|
||||
New function added to the core API: `listPeerDependencyIssues()`.
|
||||
New function added to the core API: `getPeerDependencyIssues()`.
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
"ramda": "^0.27.1",
|
||||
"run-groups": "^3.0.1",
|
||||
"semver": "^7.3.4",
|
||||
"semver-intersect": "^1.4.0",
|
||||
"version-selector-type": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,7 +3,7 @@ import link from './link'
|
||||
export * from './install'
|
||||
export { PeerDependencyIssuesError } from './install/reportPeerDependencyIssues'
|
||||
export * from './link'
|
||||
export * from './listPeerDependencyIssues'
|
||||
export * from './getPeerDependencyIssues'
|
||||
export {
|
||||
link,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { createReadPackageHook } from './install'
|
||||
import { getPreferredVersionsFromLockfile } from './install/getPreferredVersions'
|
||||
import { InstallOptions } from './install/extendInstallOptions'
|
||||
import { DEFAULT_REGISTRIES } from '@pnpm/normalize-registries'
|
||||
import { intersect } from 'semver-intersect'
|
||||
|
||||
export type ListMissingPeersOptions = Partial<GetContextOptions>
|
||||
& Pick<InstallOptions, 'hooks'
|
||||
@@ -21,10 +20,10 @@ export type ListMissingPeersOptions = Partial<GetContextOptions>
|
||||
>
|
||||
& Pick<GetContextOptions, 'storeDir'>
|
||||
|
||||
export async function listPeerDependencyIssues (
|
||||
export async function getPeerDependencyIssues (
|
||||
projects: ProjectOptions[],
|
||||
opts: ListMissingPeersOptions
|
||||
) {
|
||||
): Promise<PeerDependencyIssues> {
|
||||
const lockfileDir = opts.lockfileDir ?? process.cwd()
|
||||
const ctx = await getContext(projects, {
|
||||
force: false,
|
||||
@@ -78,34 +77,7 @@ export async function listPeerDependencyIssues (
|
||||
}
|
||||
)
|
||||
|
||||
const conflicts = getPeerDependencyConflicts(peerDependencyIssues)
|
||||
|
||||
await waitTillAllFetchingsFinish()
|
||||
|
||||
return {
|
||||
issues: peerDependencyIssues,
|
||||
conflicts,
|
||||
}
|
||||
}
|
||||
|
||||
function getPeerDependencyConflicts (peerDependencyIssues: PeerDependencyIssues) {
|
||||
const missingPeers = new Map<string, string[]>()
|
||||
for (const [peerName, issues] of Object.entries(peerDependencyIssues.missing)) {
|
||||
missingPeers.set(peerName, issues.map(({ wantedRange }) => wantedRange))
|
||||
}
|
||||
const conflicts = [] as string[]
|
||||
for (const [peerName, ranges] of missingPeers) {
|
||||
if (!intersectSafe(ranges)) {
|
||||
conflicts.push(peerName)
|
||||
}
|
||||
}
|
||||
return conflicts
|
||||
}
|
||||
|
||||
function intersectSafe (ranges: string[]) {
|
||||
try {
|
||||
return intersect(...ranges)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return peerDependencyIssues
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { listPeerDependencyIssues } from '@pnpm/core'
|
||||
import { getPeerDependencyIssues } from '@pnpm/core'
|
||||
import { prepareEmpty } from '@pnpm/prepare'
|
||||
import { testDefaults } from './utils'
|
||||
|
||||
test('cannot resolve peer dependency for top-level dependency', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
const peerDependencyIssues = await listPeerDependencyIssues([
|
||||
const peerDependencyIssues = await getPeerDependencyIssues([
|
||||
{
|
||||
manifest: {
|
||||
dependencies: {
|
||||
@@ -16,13 +16,13 @@ test('cannot resolve peer dependency for top-level dependency', async () => {
|
||||
},
|
||||
], await testDefaults())
|
||||
|
||||
expect(peerDependencyIssues.issues.missing).toHaveProperty('ajv')
|
||||
expect(peerDependencyIssues.missing).toHaveProperty('ajv')
|
||||
})
|
||||
|
||||
test('a conflict is detected when the same peer is required with ranges that do not overlap', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
const peerDependencyIssues = await listPeerDependencyIssues([
|
||||
const peerDependencyIssues = await getPeerDependencyIssues([
|
||||
{
|
||||
manifest: {
|
||||
dependencies: {
|
||||
@@ -34,5 +34,5 @@ test('a conflict is detected when the same peer is required with ranges that do
|
||||
},
|
||||
], await testDefaults())
|
||||
|
||||
expect(peerDependencyIssues.conflicts.length).toBe(1)
|
||||
expect(peerDependencyIssues.missingMergedByProjects['.'].conflicts.length).toBe(1)
|
||||
})
|
||||
@@ -169,7 +169,7 @@ test('warning is reported when cannot resolve peer dependency for top-level depe
|
||||
missing: {
|
||||
ajv: [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
projectId: '.',
|
||||
parents: [
|
||||
{
|
||||
name: 'ajv-keywords',
|
||||
@@ -180,6 +180,14 @@ test('warning is reported when cannot resolve peer dependency for top-level depe
|
||||
wantedRange: '>=4.10.0',
|
||||
}],
|
||||
},
|
||||
missingMergedByProjects: {
|
||||
'.': {
|
||||
conflicts: [],
|
||||
intersections: [
|
||||
{ peerName: 'ajv', versionRange: '>=4.10.0' },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -199,7 +207,7 @@ test('strict-peer-dependencies: error is thrown when cannot resolve peer depende
|
||||
missing: {
|
||||
ajv: [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
projectId: '.',
|
||||
parents: [
|
||||
{
|
||||
name: 'ajv-keywords',
|
||||
@@ -210,6 +218,14 @@ test('strict-peer-dependencies: error is thrown when cannot resolve peer depende
|
||||
wantedRange: '>=4.10.0',
|
||||
}],
|
||||
},
|
||||
missingMergedByProjects: {
|
||||
'.': {
|
||||
conflicts: [],
|
||||
intersections: [
|
||||
{ peerName: 'ajv', versionRange: '>=4.10.0' },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -315,7 +331,7 @@ test('warning is reported when cannot resolve peer dependency for non-top-level
|
||||
missing: {
|
||||
'peer-c': [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
projectId: '.',
|
||||
parents: [
|
||||
{
|
||||
name: 'abc-grand-parent-without-c',
|
||||
@@ -334,6 +350,14 @@ test('warning is reported when cannot resolve peer dependency for non-top-level
|
||||
wantedRange: '^1.0.0',
|
||||
}],
|
||||
},
|
||||
missingMergedByProjects: {
|
||||
'.': {
|
||||
conflicts: [],
|
||||
intersections: [
|
||||
{ peerName: 'peer-c', versionRange: '^1.0.0' },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -353,7 +377,7 @@ test('warning is reported when bad version of resolved peer dependency for non-t
|
||||
bad: {
|
||||
'peer-c': [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
projectId: '.',
|
||||
parents: [
|
||||
{
|
||||
name: 'abc-grand-parent-without-c',
|
||||
@@ -374,6 +398,7 @@ test('warning is reported when bad version of resolved peer dependency for non-t
|
||||
}],
|
||||
},
|
||||
missing: {},
|
||||
missingMergedByProjects: {},
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -393,7 +418,7 @@ test('strict-peer-dependencies: error is thrown when bad version of resolved pee
|
||||
bad: {
|
||||
'peer-c': [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
projectId: '.',
|
||||
parents: [
|
||||
{
|
||||
name: 'abc-grand-parent-without-c',
|
||||
@@ -414,6 +439,7 @@ test('strict-peer-dependencies: error is thrown when bad version of resolved pee
|
||||
}],
|
||||
},
|
||||
missing: {},
|
||||
missingMergedByProjects: {},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -987,7 +1013,7 @@ test('warning is not reported when cannot resolve optional peer dependency', asy
|
||||
bad: {
|
||||
'peer-c': [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
projectId: '.',
|
||||
parents: [
|
||||
{
|
||||
name: 'abc-optional-peers',
|
||||
@@ -1002,7 +1028,7 @@ test('warning is not reported when cannot resolve optional peer dependency', asy
|
||||
missing: {
|
||||
'peer-a': [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
projectId: '.',
|
||||
parents: [
|
||||
{
|
||||
name: 'abc-optional-peers',
|
||||
@@ -1013,6 +1039,17 @@ test('warning is not reported when cannot resolve optional peer dependency', asy
|
||||
wantedRange: '^1.0.0',
|
||||
}],
|
||||
},
|
||||
missingMergedByProjects: {
|
||||
'.': {
|
||||
conflicts: [],
|
||||
intersections: [
|
||||
{
|
||||
peerName: 'peer-a',
|
||||
versionRange: '^1.0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1043,7 +1080,7 @@ test('warning is not reported when cannot resolve optional peer dependency (spec
|
||||
missing: {
|
||||
'peer-a': [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
projectId: '.',
|
||||
parents: [
|
||||
{
|
||||
name: 'abc-optional-peers-meta-only',
|
||||
@@ -1054,6 +1091,17 @@ test('warning is not reported when cannot resolve optional peer dependency (spec
|
||||
wantedRange: '^1.0.0',
|
||||
}],
|
||||
},
|
||||
missingMergedByProjects: {
|
||||
'.': {
|
||||
conflicts: [],
|
||||
intersections: [
|
||||
{
|
||||
peerName: 'peer-a',
|
||||
versionRange: '^1.0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -25,13 +25,19 @@ test('print peer dependency issues warning', (done) => {
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
projectPath: '',
|
||||
projectId: '.',
|
||||
},
|
||||
foundVersion: '2',
|
||||
wantedRange: '3',
|
||||
},
|
||||
],
|
||||
},
|
||||
missingMergedByProjects: {
|
||||
'.': {
|
||||
conflicts: [],
|
||||
intersections: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect.assertions(1)
|
||||
@@ -40,7 +46,7 @@ test('print peer dependency issues warning', (done) => {
|
||||
complete: () => done(),
|
||||
error: done,
|
||||
next: output => {
|
||||
expect(output).toContain('<ROOT>')
|
||||
expect(output).toContain('.')
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -65,12 +71,18 @@ test('print peer dependency issues error', (done) => {
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
projectPath: '',
|
||||
projectId: '.',
|
||||
},
|
||||
wantedRange: '3',
|
||||
},
|
||||
],
|
||||
},
|
||||
missingMergedByProjects: {
|
||||
'.': {
|
||||
conflicts: [],
|
||||
intersections: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
logger.error(err, err)
|
||||
|
||||
@@ -82,7 +94,7 @@ test('print peer dependency issues error', (done) => {
|
||||
complete: () => done(),
|
||||
error: done,
|
||||
next: output => {
|
||||
expect(output).toContain('<ROOT>')
|
||||
expect(output).toContain('.')
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,32 +2,45 @@ import { PeerDependencyIssues } from '@pnpm/types'
|
||||
import archy from 'archy'
|
||||
import chalk from 'chalk'
|
||||
|
||||
const ROOT_LABEL = '<ROOT>'
|
||||
|
||||
export default function (peerDependencyIssues: PeerDependencyIssues) {
|
||||
export default function (
|
||||
{
|
||||
bad,
|
||||
missing,
|
||||
missingMergedByProjects,
|
||||
}: PeerDependencyIssues
|
||||
) {
|
||||
const projects = {} as Record<string, PkgNode>
|
||||
for (const [peerName, issues] of Object.entries(peerDependencyIssues.missing)) {
|
||||
for (const [peerName, issues] of Object.entries(missing)) {
|
||||
for (const issue of issues) {
|
||||
const projectPath = issue.location.projectPath || ROOT_LABEL
|
||||
if (!projects[projectPath]) {
|
||||
projects[projectPath] = { dependencies: {}, peerIssues: [] }
|
||||
const projectId = issue.location.projectId
|
||||
if (!projects[projectId]) {
|
||||
projects[projectId] = { dependencies: {}, peerIssues: [] }
|
||||
}
|
||||
createTree(projects[projectPath], issue.location.parents, `${chalk.red('✕ missing peer')} ${peerName}@"${issue.wantedRange}"`)
|
||||
createTree(projects[projectId], issue.location.parents, `${chalk.red('✕ missing peer')} ${peerName}@"${issue.wantedRange}"`)
|
||||
}
|
||||
}
|
||||
for (const [peerName, issues] of Object.entries(peerDependencyIssues.bad)) {
|
||||
for (const [peerName, issues] of Object.entries(bad)) {
|
||||
for (const issue of issues) {
|
||||
const projectPath = issue.location.projectPath || ROOT_LABEL
|
||||
if (!projects[projectPath]) {
|
||||
projects[projectPath] = { dependencies: {}, peerIssues: [] }
|
||||
const projectId = issue.location.projectId
|
||||
if (!projects[projectId]) {
|
||||
projects[projectId] = { dependencies: {}, peerIssues: [] }
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
createTree(projects[projectPath], issue.location.parents, `${chalk.red('✕ unmet peer')} ${peerName}@"${issue.wantedRange}": found ${issue.foundVersion}`)
|
||||
createTree(projects[projectId], issue.location.parents, `${chalk.red('✕ unmet peer')} ${peerName}@"${issue.wantedRange}": found ${issue.foundVersion}`)
|
||||
}
|
||||
}
|
||||
return Object.entries(projects)
|
||||
.sort(([projectKey1], [projectKey2]) => projectKey1.localeCompare(projectKey2))
|
||||
.map(([projectKey, project]) => archy(toArchyData(projectKey, project))).join('')
|
||||
.map(([projectKey, project]) => {
|
||||
let label = projectKey
|
||||
for (const conflict of missingMergedByProjects[projectKey].conflicts) {
|
||||
label += `\n${chalk.red(`✕ conflicting ranges for ${conflict}`)}`
|
||||
}
|
||||
for (const { peerName, versionRange } of missingMergedByProjects[projectKey].intersections) {
|
||||
label += `\nadd ${peerName}@"${versionRange}"`
|
||||
}
|
||||
return archy(toArchyData(label, project))
|
||||
}).join('')
|
||||
}
|
||||
|
||||
interface PkgNode {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renderPeerIssues() 1`] = `
|
||||
"/packages/0
|
||||
└─┬ zzz
|
||||
└── ✕ missing peer ddd@\\"^1.0.0\\"
|
||||
<ROOT>
|
||||
".
|
||||
└─┬ xxx
|
||||
├── ✕ unmet peer bbb@\\"^1.0.0\\": found 2
|
||||
└─┬ yyy
|
||||
├── ✕ missing peer aaa@\\"^1.0.0\\"
|
||||
└── ✕ unmet peer ccc@\\"^1.0.0\\": found 2
|
||||
packages/0
|
||||
└─┬ zzz
|
||||
└── ✕ missing peer ddd@\\"^1.0.0\\"
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -17,7 +17,7 @@ test('renderPeerIssues()', () => {
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
projectPath: '',
|
||||
projectId: '.',
|
||||
},
|
||||
wantedRange: '^1.0.0',
|
||||
},
|
||||
@@ -31,7 +31,7 @@ test('renderPeerIssues()', () => {
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
projectPath: '/packages/0',
|
||||
projectId: 'packages/0',
|
||||
},
|
||||
wantedRange: '^1.0.0',
|
||||
},
|
||||
@@ -47,7 +47,7 @@ test('renderPeerIssues()', () => {
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
projectPath: '',
|
||||
projectId: '.',
|
||||
},
|
||||
foundVersion: '2',
|
||||
wantedRange: '^1.0.0',
|
||||
@@ -66,12 +66,22 @@ test('renderPeerIssues()', () => {
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
projectPath: '',
|
||||
projectId: '.',
|
||||
},
|
||||
foundVersion: '2',
|
||||
wantedRange: '^1.0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
missingMergedByProjects: {
|
||||
'.': {
|
||||
conflicts: [],
|
||||
intersections: [],
|
||||
},
|
||||
'packages/0': {
|
||||
conflicts: [],
|
||||
intersections: [],
|
||||
},
|
||||
},
|
||||
}))).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"ramda": "^0.27.1",
|
||||
"replace-string": "^3.1.0",
|
||||
"semver": "^7.3.4",
|
||||
"semver-range-intersect": "^0.3.1",
|
||||
"version-selector-type": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
31
packages/resolve-dependencies/src/mergePeersByProjects.ts
Normal file
31
packages/resolve-dependencies/src/mergePeersByProjects.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { MergedPeersByProjects } from '@pnpm/types'
|
||||
import { intersect } from 'semver-range-intersect'
|
||||
|
||||
export function mergePeersByProjects (missingPeersByProject: Record<string, Record<string, string[]>>): MergedPeersByProjects {
|
||||
const mergedPeersByProjects: MergedPeersByProjects = {}
|
||||
for (const [projectPath, rangesByPeerNames] of Object.entries(missingPeersByProject)) {
|
||||
mergedPeersByProjects[projectPath] = {
|
||||
conflicts: [],
|
||||
intersections: [],
|
||||
}
|
||||
for (const [peerName, ranges] of Object.entries(rangesByPeerNames)) {
|
||||
if (ranges.length === 1) {
|
||||
mergedPeersByProjects[projectPath].intersections.push({
|
||||
peerName,
|
||||
versionRange: ranges[0],
|
||||
})
|
||||
continue
|
||||
}
|
||||
const intersection = intersect(...ranges)
|
||||
if (intersection === null) {
|
||||
mergedPeersByProjects[projectPath].conflicts.push(peerName)
|
||||
} else {
|
||||
mergedPeersByProjects[projectPath].intersections.push({
|
||||
peerName,
|
||||
versionRange: intersection,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return mergedPeersByProjects
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import crypto from 'crypto'
|
||||
import path from 'path'
|
||||
import { Dependencies, PeerDependencyIssues } from '@pnpm/types'
|
||||
import {
|
||||
BadPeerIssuesByPeerName,
|
||||
MissingPeerIssuesByPeerName,
|
||||
Dependencies,
|
||||
PeerDependencyIssues,
|
||||
} from '@pnpm/types'
|
||||
import { depPathToFilename } from 'dependency-path'
|
||||
import { KeyValuePair } from 'ramda'
|
||||
import fromPairs from 'ramda/src/fromPairs'
|
||||
@@ -13,6 +18,7 @@ import {
|
||||
DependenciesTreeNode,
|
||||
ResolvedPackage,
|
||||
} from './resolveDependencies'
|
||||
import { mergePeersByProjects } from './mergePeersByProjects'
|
||||
import { createNodeId, splitNodeId } from './nodeIdUtils'
|
||||
|
||||
export interface GenericDependenciesGraphNode {
|
||||
@@ -64,10 +70,9 @@ export default function<T extends PartialResolvedPackage> (
|
||||
const _createPkgsByName = createPkgsByName.bind(null, opts.dependenciesTree)
|
||||
const rootProject = opts.projects.length > 1 ? opts.projects.find(({ id }) => id === '.') : null
|
||||
const rootPkgsByName = rootProject == null ? {} : _createPkgsByName(rootProject)
|
||||
const peerDependencyIssues: PeerDependencyIssues = {
|
||||
bad: {},
|
||||
missing: {},
|
||||
}
|
||||
const badPeers: BadPeerIssuesByPeerName = {}
|
||||
const missingPeers: MissingPeerIssuesByPeerName = {}
|
||||
const missingPeersByProject = {} as Record<string, Record<string, string[]>>
|
||||
|
||||
for (const { directNodeIdsByAlias, topParents, rootDir } of opts.projects) {
|
||||
const pkgsByName = {
|
||||
@@ -76,11 +81,13 @@ export default function<T extends PartialResolvedPackage> (
|
||||
}
|
||||
|
||||
resolvePeersOfChildren(directNodeIdsByAlias, pkgsByName, {
|
||||
badPeers,
|
||||
missingPeers,
|
||||
dependenciesTree: opts.dependenciesTree,
|
||||
depGraph,
|
||||
lockfileDir: opts.lockfileDir,
|
||||
missingPeersByProject,
|
||||
pathsByNodeId,
|
||||
peerDependencyIssues,
|
||||
peersCache: new Map(),
|
||||
purePkgs: new Set(),
|
||||
rootDir,
|
||||
@@ -105,7 +112,11 @@ export default function<T extends PartialResolvedPackage> (
|
||||
return {
|
||||
dependenciesGraph: depGraph,
|
||||
dependenciesByProjectId,
|
||||
peerDependencyIssues,
|
||||
peerDependencyIssues: {
|
||||
bad: badPeers,
|
||||
missing: missingPeers,
|
||||
missingMergedByProjects: mergePeersByProjects(missingPeersByProject),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +170,9 @@ function resolvePeersOfNode<T extends PartialResolvedPackage> (
|
||||
pathsByNodeId: {[nodeId: string]: string}
|
||||
depGraph: GenericDependenciesGraph<T>
|
||||
virtualStoreDir: string
|
||||
peerDependencyIssues: PeerDependencyIssues
|
||||
badPeers: BadPeerIssuesByPeerName
|
||||
missingPeers: MissingPeerIssuesByPeerName
|
||||
missingPeersByProject: Record<string, Record<string, string[]>>
|
||||
peersCache: PeersCache
|
||||
purePkgs: Set<string> // pure packages are those that don't rely on externally resolved peers
|
||||
rootDir: string
|
||||
@@ -231,7 +244,9 @@ function resolvePeersOfNode<T extends PartialResolvedPackage> (
|
||||
lockfileDir: ctx.lockfileDir,
|
||||
nodeId,
|
||||
parentPkgs,
|
||||
peerDependencyIssues: ctx.peerDependencyIssues,
|
||||
badPeers: ctx.badPeers,
|
||||
missingPeers: ctx.missingPeers,
|
||||
missingPeersByProject: ctx.missingPeersByProject,
|
||||
resolvedPackage,
|
||||
rootDir: ctx.rootDir,
|
||||
})
|
||||
@@ -339,7 +354,9 @@ function resolvePeersOfChildren<T extends PartialResolvedPackage> (
|
||||
parentPkgs: ParentRefs,
|
||||
ctx: {
|
||||
pathsByNodeId: {[nodeId: string]: string}
|
||||
peerDependencyIssues: PeerDependencyIssues
|
||||
badPeers: BadPeerIssuesByPeerName
|
||||
missingPeers: MissingPeerIssuesByPeerName
|
||||
missingPeersByProject: Record<string, Record<string, string[]>>
|
||||
peersCache: PeersCache
|
||||
virtualStoreDir: string
|
||||
purePkgs: Set<string>
|
||||
@@ -377,7 +394,9 @@ function resolvePeers<T extends PartialResolvedPackage> (
|
||||
resolvedPackage: T
|
||||
dependenciesTree: DependenciesTree<T>
|
||||
rootDir: string
|
||||
peerDependencyIssues: PeerDependencyIssues
|
||||
badPeers: BadPeerIssuesByPeerName
|
||||
missingPeers: MissingPeerIssuesByPeerName
|
||||
missingPeersByProject: Record<string, Record<string, string[]>>
|
||||
}
|
||||
): PeersResolution {
|
||||
const resolvedPeers: {[alias: string]: string} = {}
|
||||
@@ -394,32 +413,36 @@ function resolvePeers<T extends PartialResolvedPackage> (
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (!ctx.peerDependencyIssues.missing[peerName]) {
|
||||
ctx.peerDependencyIssues.missing[peerName] = []
|
||||
if (!ctx.missingPeers[peerName]) {
|
||||
ctx.missingPeers[peerName] = []
|
||||
}
|
||||
ctx.peerDependencyIssues.missing[peerName].push({
|
||||
const issue = {
|
||||
location: getLocationFromNodeId({
|
||||
dependenciesTree: ctx.dependenciesTree,
|
||||
nodeId: ctx.nodeId,
|
||||
lockfileDir: ctx.lockfileDir,
|
||||
rootDir: ctx.rootDir,
|
||||
pkg: ctx.resolvedPackage,
|
||||
}),
|
||||
wantedRange: peerVersionRange,
|
||||
})
|
||||
}
|
||||
ctx.missingPeers[peerName].push(issue)
|
||||
if (!ctx.missingPeersByProject[issue.location.projectId]) {
|
||||
ctx.missingPeersByProject[issue.location.projectId] = {}
|
||||
}
|
||||
if (!ctx.missingPeersByProject[issue.location.projectId][peerName]) {
|
||||
ctx.missingPeersByProject[issue.location.projectId][peerName] = []
|
||||
}
|
||||
ctx.missingPeersByProject[issue.location.projectId][peerName].push(peerVersionRange)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!semver.satisfies(resolved.version, peerVersionRange, { loose: true })) {
|
||||
if (!ctx.peerDependencyIssues.bad[peerName]) {
|
||||
ctx.peerDependencyIssues.bad[peerName] = []
|
||||
if (!ctx.badPeers[peerName]) {
|
||||
ctx.badPeers[peerName] = []
|
||||
}
|
||||
ctx.peerDependencyIssues.bad[peerName].push({
|
||||
ctx.badPeers[peerName].push({
|
||||
location: getLocationFromNodeId({
|
||||
dependenciesTree: ctx.dependenciesTree,
|
||||
nodeId: ctx.nodeId,
|
||||
lockfileDir: ctx.lockfileDir,
|
||||
rootDir: ctx.rootDir,
|
||||
pkg: ctx.resolvedPackage,
|
||||
}),
|
||||
foundVersion: resolved.version,
|
||||
@@ -435,16 +458,12 @@ function resolvePeers<T extends PartialResolvedPackage> (
|
||||
function getLocationFromNodeId<T> (
|
||||
{
|
||||
dependenciesTree,
|
||||
lockfileDir,
|
||||
nodeId,
|
||||
rootDir,
|
||||
pkg,
|
||||
}: {
|
||||
dependenciesTree: DependenciesTree<T>
|
||||
lockfileDir: string
|
||||
nodeId: string
|
||||
pkg: PartialResolvedPackage
|
||||
rootDir: string
|
||||
}
|
||||
) {
|
||||
const parts = splitNodeId(nodeId).slice(0, -1)
|
||||
@@ -452,9 +471,8 @@ function getLocationFromNodeId<T> (
|
||||
.slice(2)
|
||||
.map((nid) => pick(['name', 'version'], dependenciesTree[nid].resolvedPackage as ResolvedPackage))
|
||||
parents.push({ name: pkg.name, version: pkg.version })
|
||||
const projectPath = path.relative(lockfileDir, rootDir)
|
||||
return {
|
||||
projectPath,
|
||||
projectId: parts[0],
|
||||
parents,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,3 +201,121 @@ test('when a package is referenced twice in the dependencies graph and one of th
|
||||
'bar/1.0.0',
|
||||
])
|
||||
})
|
||||
|
||||
describe('peer dependency issues', () => {
|
||||
const fooPkg = {
|
||||
name: 'foo',
|
||||
depPath: 'foo/1.0.0',
|
||||
version: '1.0.0',
|
||||
peerDependencies: {
|
||||
peer: '1',
|
||||
},
|
||||
}
|
||||
const barPkg = {
|
||||
name: 'bar',
|
||||
depPath: 'bar/1.0.0',
|
||||
version: '1.0.0',
|
||||
peerDependencies: {
|
||||
peer: '2',
|
||||
},
|
||||
}
|
||||
const qarPkg = {
|
||||
name: 'qar',
|
||||
depPath: 'qar/1.0.0',
|
||||
version: '1.0.0',
|
||||
peerDependencies: {
|
||||
peer: '^2.2.0',
|
||||
},
|
||||
}
|
||||
const { peerDependencyIssues } = resolvePeers({
|
||||
projects: [
|
||||
{
|
||||
directNodeIdsByAlias: {
|
||||
foo: '>project1>foo/1.0.0',
|
||||
},
|
||||
topParents: [],
|
||||
rootDir: '',
|
||||
id: 'project1',
|
||||
},
|
||||
{
|
||||
directNodeIdsByAlias: {
|
||||
bar: '>project2>bar/1.0.0',
|
||||
},
|
||||
topParents: [],
|
||||
rootDir: '',
|
||||
id: 'project2',
|
||||
},
|
||||
{
|
||||
directNodeIdsByAlias: {
|
||||
foo: '>project3>foo/1.0.0',
|
||||
bar: '>project3>bar/1.0.0',
|
||||
},
|
||||
topParents: [],
|
||||
rootDir: '',
|
||||
id: 'project3',
|
||||
},
|
||||
{
|
||||
directNodeIdsByAlias: {
|
||||
bar: '>project4>bar/1.0.0',
|
||||
qar: '>project4>qar/1.0.0',
|
||||
},
|
||||
topParents: [],
|
||||
rootDir: '',
|
||||
id: 'project4',
|
||||
},
|
||||
],
|
||||
dependenciesTree: {
|
||||
'>project1>foo/1.0.0': {
|
||||
children: {},
|
||||
installable: true,
|
||||
resolvedPackage: fooPkg,
|
||||
depth: 0,
|
||||
},
|
||||
'>project2>bar/1.0.0': {
|
||||
children: {},
|
||||
installable: true,
|
||||
resolvedPackage: barPkg,
|
||||
depth: 0,
|
||||
},
|
||||
'>project3>foo/1.0.0': {
|
||||
children: {},
|
||||
installable: true,
|
||||
resolvedPackage: fooPkg,
|
||||
depth: 0,
|
||||
},
|
||||
'>project3>bar/1.0.0': {
|
||||
children: {},
|
||||
installable: true,
|
||||
resolvedPackage: barPkg,
|
||||
depth: 0,
|
||||
},
|
||||
'>project4>bar/1.0.0': {
|
||||
children: {},
|
||||
installable: true,
|
||||
resolvedPackage: barPkg,
|
||||
depth: 0,
|
||||
},
|
||||
'>project4>qar/1.0.0': {
|
||||
children: {},
|
||||
installable: true,
|
||||
resolvedPackage: qarPkg,
|
||||
depth: 0,
|
||||
},
|
||||
},
|
||||
virtualStoreDir: '',
|
||||
lockfileDir: '',
|
||||
})
|
||||
it('should find peer dependency conflicts', () => {
|
||||
expect(peerDependencyIssues.missingMergedByProjects['project3'].conflicts).toStrictEqual(['peer'])
|
||||
})
|
||||
it('should pick the single wanted peer dependency range', () => {
|
||||
expect(peerDependencyIssues.missingMergedByProjects['project1'].intersections)
|
||||
.toStrictEqual([{ peerName: 'peer', versionRange: '1' }])
|
||||
expect(peerDependencyIssues.missingMergedByProjects['project2'].intersections)
|
||||
.toStrictEqual([{ peerName: 'peer', versionRange: '2' }])
|
||||
})
|
||||
it('should return the intersection of two compatible ranges', () => {
|
||||
expect(peerDependencyIssues.missingMergedByProjects['project4'].intersections)
|
||||
.toStrictEqual([{ peerName: 'peer', versionRange: '>=2.2.0 <3.0.0' }])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface PeerDependencyIssueLocation {
|
||||
parents: Array<{ name: string, version: string }>
|
||||
projectPath: string
|
||||
projectId: string
|
||||
}
|
||||
|
||||
export interface MissingPeerDependencyIssue {
|
||||
@@ -8,11 +8,28 @@ export interface MissingPeerDependencyIssue {
|
||||
wantedRange: string
|
||||
}
|
||||
|
||||
export type MissingPeerIssuesByPeerName = Record<string, MissingPeerDependencyIssue[]>
|
||||
|
||||
export interface BadPeerDependencyIssue extends MissingPeerDependencyIssue {
|
||||
foundVersion: string
|
||||
}
|
||||
|
||||
export type BadPeerIssuesByPeerName = Record<string, BadPeerDependencyIssue[]>
|
||||
|
||||
export interface PeerDependencyIssues {
|
||||
bad: Record<string, BadPeerDependencyIssue[]>
|
||||
missing: Record<string, MissingPeerDependencyIssue[]>
|
||||
bad: BadPeerIssuesByPeerName
|
||||
missing: MissingPeerIssuesByPeerName
|
||||
missingMergedByProjects: MergedPeersByProjects
|
||||
}
|
||||
|
||||
export type MergedPeersByProjects = Record<string, MergedPeers>
|
||||
|
||||
export interface MergedPeers {
|
||||
conflicts: string[]
|
||||
intersections: PeerIntersection[]
|
||||
}
|
||||
|
||||
export interface PeerIntersection {
|
||||
peerName: string
|
||||
versionRange: string
|
||||
}
|
||||
|
||||
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
@@ -451,7 +451,6 @@ importers:
|
||||
resolve-link-target: ^2.0.0
|
||||
run-groups: ^3.0.1
|
||||
semver: ^7.3.4
|
||||
semver-intersect: ^1.4.0
|
||||
sinon: ^11.1.1
|
||||
symlink-dir: ^5.0.0
|
||||
version-selector-type: ^3.0.0
|
||||
@@ -505,7 +504,6 @@ importers:
|
||||
ramda: 0.27.1
|
||||
run-groups: 3.0.1
|
||||
semver: 7.3.5
|
||||
semver-intersect: 1.4.0
|
||||
version-selector-type: 3.0.0
|
||||
devDependencies:
|
||||
'@pnpm/assert-project': link:../../privatePackages/assert-project
|
||||
@@ -3104,6 +3102,7 @@ importers:
|
||||
ramda: ^0.27.1
|
||||
replace-string: ^3.1.0
|
||||
semver: ^7.3.4
|
||||
semver-range-intersect: ^0.3.1
|
||||
version-selector-type: ^3.0.0
|
||||
dependencies:
|
||||
'@pnpm/constants': link:../constants
|
||||
@@ -3129,6 +3128,7 @@ importers:
|
||||
ramda: 0.27.1
|
||||
replace-string: 3.1.0
|
||||
semver: 7.3.5
|
||||
semver-range-intersect: 0.3.1
|
||||
version-selector-type: 3.0.0
|
||||
devDependencies:
|
||||
'@pnpm/logger': 4.0.0
|
||||
@@ -5331,7 +5331,6 @@ packages:
|
||||
|
||||
/@types/semver/6.2.3:
|
||||
resolution: {integrity: sha512-KQf+QAMWKMrtBMsB8/24w53tEsxllMj6TuA80TT/5igJalLI/zm0L3oXRbIAl4Ohfc85gyHX/jhMwsVkmhLU4A==}
|
||||
dev: true
|
||||
|
||||
/@types/semver/7.3.9:
|
||||
resolution: {integrity: sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==}
|
||||
@@ -13836,10 +13835,12 @@ packages:
|
||||
xmlchars: 2.2.0
|
||||
dev: true
|
||||
|
||||
/semver-intersect/1.4.0:
|
||||
resolution: {integrity: sha512-d8fvGg5ycKAq0+I6nfWeCx6ffaWJCsBYU0H2Rq56+/zFePYfT8mXkB3tWBSjR5BerkHNZ5eTPIk1/LBYas35xQ==}
|
||||
/semver-range-intersect/0.3.1:
|
||||
resolution: {integrity: sha512-dZAVI9Gdl3uBvs1CBK1KHeCyiZDn4X14DW4C+QFQj+0k+l9L+pY1swt4KVt1hGU2dP77but4vx+N5XeYQsDteQ==}
|
||||
engines: {node: '>=8.3.0'}
|
||||
dependencies:
|
||||
semver: 5.7.1
|
||||
'@types/semver': 6.2.3
|
||||
semver: 6.3.0
|
||||
dev: false
|
||||
|
||||
/semver-utils/1.1.4:
|
||||
|
||||
4
typings/typed.d.ts
vendored
4
typings/typed.d.ts
vendored
@@ -60,7 +60,3 @@ declare module 'bin-links/lib/fix-bin' {
|
||||
declare namespace NodeJS.Module {
|
||||
function _nodeModulePaths(from: string): string[]
|
||||
}
|
||||
|
||||
declare module 'semver-intersect' {
|
||||
export function intersect (...range: string[]): string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user