diff --git a/.changeset/tasty-kiwis-boil.md b/.changeset/tasty-kiwis-boil.md index 6714586fc8..1249cc49ea 100644 --- a/.changeset/tasty-kiwis-boil.md +++ b/.changeset/tasty-kiwis-boil.md @@ -2,4 +2,4 @@ "@pnpm/core": minor --- -New function added to the core API: `listPeerDependencyIssues()`. +New function added to the core API: `getPeerDependencyIssues()`. diff --git a/packages/core/package.json b/packages/core/package.json index f5a4d05452..e9f83032da 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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": { diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index 6827278f55..880417cc45 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -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, } diff --git a/packages/core/src/listPeerDependencyIssues.ts b/packages/core/src/getPeerDependencyIssues.ts similarity index 77% rename from packages/core/src/listPeerDependencyIssues.ts rename to packages/core/src/getPeerDependencyIssues.ts index 702992d2f3..5addde0cb5 100644 --- a/packages/core/src/listPeerDependencyIssues.ts +++ b/packages/core/src/getPeerDependencyIssues.ts @@ -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 & Pick > & Pick -export async function listPeerDependencyIssues ( +export async function getPeerDependencyIssues ( projects: ProjectOptions[], opts: ListMissingPeersOptions -) { +): Promise { 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() - 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 } diff --git a/packages/core/test/listPeerDependencyIssues.test.ts b/packages/core/test/getPeerDependencyIssues.test.ts similarity index 68% rename from packages/core/test/listPeerDependencyIssues.test.ts rename to packages/core/test/getPeerDependencyIssues.test.ts index 4233d8778a..0324e77a99 100644 --- a/packages/core/test/listPeerDependencyIssues.test.ts +++ b/packages/core/test/getPeerDependencyIssues.test.ts @@ -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) }) diff --git a/packages/core/test/install/peerDependencies.ts b/packages/core/test/install/peerDependencies.ts index 40c4dc0cec..faeee17386 100644 --- a/packages/core/test/install/peerDependencies.ts +++ b/packages/core/test/install/peerDependencies.ts @@ -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', + }, + ], + }, + }, }) ) diff --git a/packages/default-reporter/test/reportingPeerDependencyIssues.ts b/packages/default-reporter/test/reportingPeerDependencyIssues.ts index 563141e3e8..5991ce1444 100644 --- a/packages/default-reporter/test/reportingPeerDependencyIssues.ts +++ b/packages/default-reporter/test/reportingPeerDependencyIssues.ts @@ -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('') + 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('') + expect(output).toContain('.') }, }) }) diff --git a/packages/render-peer-issues/src/index.ts b/packages/render-peer-issues/src/index.ts index 06dde07921..0619b39b03 100644 --- a/packages/render-peer-issues/src/index.ts +++ b/packages/render-peer-issues/src/index.ts @@ -2,32 +2,45 @@ import { PeerDependencyIssues } from '@pnpm/types' import archy from 'archy' import chalk from 'chalk' -const ROOT_LABEL = '' - -export default function (peerDependencyIssues: PeerDependencyIssues) { +export default function ( + { + bad, + missing, + missingMergedByProjects, + }: PeerDependencyIssues +) { const projects = {} as Record - 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 { diff --git a/packages/render-peer-issues/test/__snapshots__/index.ts.snap b/packages/render-peer-issues/test/__snapshots__/index.ts.snap index cf6dfde212..60f984078a 100644 --- a/packages/render-peer-issues/test/__snapshots__/index.ts.snap +++ b/packages/render-peer-issues/test/__snapshots__/index.ts.snap @@ -1,14 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renderPeerIssues() 1`] = ` -"/packages/0 -└─┬ zzz - └── ✕ missing peer ddd@\\"^1.0.0\\" - +". └─┬ 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\\" " `; diff --git a/packages/render-peer-issues/test/index.ts b/packages/render-peer-issues/test/index.ts index d19feaeebe..d6759d8c54 100644 --- a/packages/render-peer-issues/test/index.ts +++ b/packages/render-peer-issues/test/index.ts @@ -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() }) diff --git a/packages/resolve-dependencies/package.json b/packages/resolve-dependencies/package.json index ff6a201e8a..b076e3d41f 100644 --- a/packages/resolve-dependencies/package.json +++ b/packages/resolve-dependencies/package.json @@ -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": { diff --git a/packages/resolve-dependencies/src/mergePeersByProjects.ts b/packages/resolve-dependencies/src/mergePeersByProjects.ts new file mode 100644 index 0000000000..8e3fe9ec4d --- /dev/null +++ b/packages/resolve-dependencies/src/mergePeersByProjects.ts @@ -0,0 +1,31 @@ +import { MergedPeersByProjects } from '@pnpm/types' +import { intersect } from 'semver-range-intersect' + +export function mergePeersByProjects (missingPeersByProject: Record>): 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 +} diff --git a/packages/resolve-dependencies/src/resolvePeers.ts b/packages/resolve-dependencies/src/resolvePeers.ts index 500909cb70..d875dadc47 100644 --- a/packages/resolve-dependencies/src/resolvePeers.ts +++ b/packages/resolve-dependencies/src/resolvePeers.ts @@ -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 ( 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> for (const { directNodeIdsByAlias, topParents, rootDir } of opts.projects) { const pkgsByName = { @@ -76,11 +81,13 @@ export default function ( } 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 ( return { dependenciesGraph: depGraph, dependenciesByProjectId, - peerDependencyIssues, + peerDependencyIssues: { + bad: badPeers, + missing: missingPeers, + missingMergedByProjects: mergePeersByProjects(missingPeersByProject), + }, } } @@ -159,7 +170,9 @@ function resolvePeersOfNode ( pathsByNodeId: {[nodeId: string]: string} depGraph: GenericDependenciesGraph virtualStoreDir: string - peerDependencyIssues: PeerDependencyIssues + badPeers: BadPeerIssuesByPeerName + missingPeers: MissingPeerIssuesByPeerName + missingPeersByProject: Record> peersCache: PeersCache purePkgs: Set // pure packages are those that don't rely on externally resolved peers rootDir: string @@ -231,7 +244,9 @@ function resolvePeersOfNode ( 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 ( parentPkgs: ParentRefs, ctx: { pathsByNodeId: {[nodeId: string]: string} - peerDependencyIssues: PeerDependencyIssues + badPeers: BadPeerIssuesByPeerName + missingPeers: MissingPeerIssuesByPeerName + missingPeersByProject: Record> peersCache: PeersCache virtualStoreDir: string purePkgs: Set @@ -377,7 +394,9 @@ function resolvePeers ( resolvedPackage: T dependenciesTree: DependenciesTree rootDir: string - peerDependencyIssues: PeerDependencyIssues + badPeers: BadPeerIssuesByPeerName + missingPeers: MissingPeerIssuesByPeerName + missingPeersByProject: Record> } ): PeersResolution { const resolvedPeers: {[alias: string]: string} = {} @@ -394,32 +413,36 @@ function resolvePeers ( ) { 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 ( function getLocationFromNodeId ( { dependenciesTree, - lockfileDir, nodeId, - rootDir, pkg, }: { dependenciesTree: DependenciesTree - lockfileDir: string nodeId: string pkg: PartialResolvedPackage - rootDir: string } ) { const parts = splitNodeId(nodeId).slice(0, -1) @@ -452,9 +471,8 @@ function getLocationFromNodeId ( .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, } } diff --git a/packages/resolve-dependencies/test/resolvePeers.ts b/packages/resolve-dependencies/test/resolvePeers.ts index ba8a824c2a..da4df68085 100644 --- a/packages/resolve-dependencies/test/resolvePeers.ts +++ b/packages/resolve-dependencies/test/resolvePeers.ts @@ -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' }]) + }) +}) diff --git a/packages/types/src/peerDependencyIssues.ts b/packages/types/src/peerDependencyIssues.ts index 9291737025..726c78db22 100644 --- a/packages/types/src/peerDependencyIssues.ts +++ b/packages/types/src/peerDependencyIssues.ts @@ -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 + export interface BadPeerDependencyIssue extends MissingPeerDependencyIssue { foundVersion: string } +export type BadPeerIssuesByPeerName = Record + export interface PeerDependencyIssues { - bad: Record - missing: Record + bad: BadPeerIssuesByPeerName + missing: MissingPeerIssuesByPeerName + missingMergedByProjects: MergedPeersByProjects +} + +export type MergedPeersByProjects = Record + +export interface MergedPeers { + conflicts: string[] + intersections: PeerIntersection[] +} + +export interface PeerIntersection { + peerName: string + versionRange: string } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2118a017b5..8aa8ddf85c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/typings/typed.d.ts b/typings/typed.d.ts index d968cae2ea..c2b8d13c32 100644 --- a/typings/typed.d.ts +++ b/typings/typed.d.ts @@ -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 -}