mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat: detect conflicting peer dependencies and print nice warnings about peer dependency issues (#4102)
This commit is contained in:
6
.changeset/beige-socks-smile.md
Normal file
6
.changeset/beige-socks-smile.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/core-loggers": minor
|
||||
"@pnpm/default-reporter": minor
|
||||
---
|
||||
|
||||
Add peerDependencyIssuesLogger.
|
||||
5
.changeset/large-buttons-lay.md
Normal file
5
.changeset/large-buttons-lay.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/render-peer-issues": major
|
||||
---
|
||||
|
||||
Initial release.
|
||||
5
.changeset/little-tools-wonder.md
Normal file
5
.changeset/little-tools-wonder.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/types": minor
|
||||
---
|
||||
|
||||
Add types for peer dependency issues.
|
||||
@@ -2,4 +2,4 @@
|
||||
"@pnpm/core": minor
|
||||
---
|
||||
|
||||
New function added to the core API: `listMissingPeers()`.
|
||||
New function added to the core API: `listPeerDependencyIssues()`.
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@commitlint/prompt-cli": "^15.0.0",
|
||||
"@pnpm/eslint-config": "workspace:*",
|
||||
"@pnpm/meta-updater": "0.0.6",
|
||||
"@pnpm/registry-mock": "^2.10.0",
|
||||
"@pnpm/registry-mock": "^2.11.0",
|
||||
"@pnpm/tsconfig": "workspace:*",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/node": "^14.17.32",
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from './lifecycleLogger'
|
||||
export * from './linkLogger'
|
||||
export * from './packageImportMethodLogger'
|
||||
export * from './packageManifestLogger'
|
||||
export * from './peerDependencyIssues'
|
||||
export * from './progressLogger'
|
||||
export * from './registryLogger'
|
||||
export * from './removalLogger'
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
LinkLog,
|
||||
PackageImportMethodLog,
|
||||
PackageManifestLog,
|
||||
PeerDependencyIssuesLog,
|
||||
ProgressLog,
|
||||
RegistryLog,
|
||||
RequestRetryLog,
|
||||
@@ -32,6 +33,7 @@ export type Log =
|
||||
| LinkLog
|
||||
| PackageManifestLog
|
||||
| PackageImportMethodLog
|
||||
| PeerDependencyIssuesLog
|
||||
| ProgressLog
|
||||
| RegistryLog
|
||||
| RequestRetryLog
|
||||
|
||||
11
packages/core-loggers/src/peerDependencyIssues.ts
Normal file
11
packages/core-loggers/src/peerDependencyIssues.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import baseLogger, {
|
||||
LogBase,
|
||||
Logger,
|
||||
} from '@pnpm/logger'
|
||||
import { PeerDependencyIssues } from '@pnpm/types'
|
||||
|
||||
export const peerDependencyIssuesLogger = baseLogger('peer-dependency-issues') as Logger<PeerDependencyIssuesMessage>
|
||||
|
||||
export type PeerDependencyIssuesMessage = PeerDependencyIssues
|
||||
|
||||
export type PeerDependencyIssuesLog = {name: 'pnpm:peer-dependency-issues'} & LogBase & PeerDependencyIssuesMessage
|
||||
@@ -62,6 +62,7 @@
|
||||
"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": {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import link from './link'
|
||||
|
||||
export * from './install'
|
||||
export { PeerDependencyIssuesError } from './install/reportPeerDependencyIssues'
|
||||
export * from './link'
|
||||
export * from './listMissingPeers'
|
||||
export * from './listPeerDependencyIssues'
|
||||
export {
|
||||
link,
|
||||
}
|
||||
|
||||
@@ -961,12 +961,10 @@ const _installInContext: InstallFunction = async (projects, ctx, opts) => {
|
||||
|
||||
await opts.storeController.close()
|
||||
|
||||
if (peerDependencyIssues.length > 0) {
|
||||
reportPeerDependencyIssues(peerDependencyIssues, {
|
||||
lockfileDir: opts.lockfileDir,
|
||||
strictPeerDependencies: opts.strictPeerDependencies,
|
||||
})
|
||||
}
|
||||
reportPeerDependencyIssues(peerDependencyIssues, {
|
||||
lockfileDir: opts.lockfileDir,
|
||||
strictPeerDependencies: opts.strictPeerDependencies,
|
||||
})
|
||||
|
||||
return {
|
||||
newLockfile,
|
||||
|
||||
@@ -1,61 +1,26 @@
|
||||
import PnpmError from '@pnpm/error'
|
||||
import logger from '@pnpm/logger'
|
||||
import { PeerDependencyIssue, PeerDependencyIssueLocation } from '@pnpm/resolve-dependencies'
|
||||
import { peerDependencyIssuesLogger } from '@pnpm/core-loggers'
|
||||
import { PeerDependencyIssues } from '@pnpm/types'
|
||||
import isEmpty from 'ramda/src/isEmpty'
|
||||
|
||||
export default function (
|
||||
peerDependencyIssues: PeerDependencyIssue[],
|
||||
peerDependencyIssues: PeerDependencyIssues,
|
||||
opts: {
|
||||
lockfileDir: string
|
||||
strictPeerDependencies: boolean
|
||||
}
|
||||
) {
|
||||
for (const peerDependencyIssue of peerDependencyIssues) {
|
||||
const message = peerDependencyIssueMessage({
|
||||
peerDependencyIssue,
|
||||
...opts,
|
||||
})
|
||||
if (opts.strictPeerDependencies) {
|
||||
const code = peerDependencyIssue.foundPeerVersion ? 'INVALID_PEER_DEPENDENCY' : 'MISSING_PEER_DEPENDENCY'
|
||||
const err = new PnpmError(code, message)
|
||||
if (peerDependencyIssues.length === 1) {
|
||||
throw err
|
||||
}
|
||||
logger.error(err)
|
||||
} else {
|
||||
logger.warn({
|
||||
message,
|
||||
prefix: peerDependencyIssue.rootDir,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (isEmpty(peerDependencyIssues.bad) && isEmpty(peerDependencyIssues.missing)) return
|
||||
if (opts.strictPeerDependencies) {
|
||||
throw new PnpmError('PEER_DEPENDENCY', `${peerDependencyIssues.length} peer dependency issues found.`)
|
||||
throw new PeerDependencyIssuesError(peerDependencyIssues)
|
||||
}
|
||||
peerDependencyIssuesLogger.debug(peerDependencyIssues)
|
||||
}
|
||||
|
||||
function peerDependencyIssueMessage (
|
||||
opts: {
|
||||
lockfileDir: string
|
||||
peerDependencyIssue: PeerDependencyIssue
|
||||
export class PeerDependencyIssuesError extends PnpmError {
|
||||
issues: PeerDependencyIssues
|
||||
constructor (issues: PeerDependencyIssues) {
|
||||
super('PEER_DEP_ISSUES', 'Unmet peer dependencies')
|
||||
this.issues = issues
|
||||
}
|
||||
) {
|
||||
const friendlyPath = locationToFriendlyPath(opts.peerDependencyIssue.location)
|
||||
if (opts.peerDependencyIssue.foundPeerVersion) {
|
||||
return `${friendlyPath ? `${friendlyPath}: ` : ''}${packageFriendlyId(opts.peerDependencyIssue.pkg)} \
|
||||
requires a peer of ${opts.peerDependencyIssue.wantedPeer.name}@${opts.peerDependencyIssue.wantedPeer.range} but version ${opts.peerDependencyIssue.foundPeerVersion} was installed.`
|
||||
}
|
||||
return `${friendlyPath ? `${friendlyPath}: ` : ''}${packageFriendlyId(opts.peerDependencyIssue.pkg)} \
|
||||
requires a peer of ${opts.peerDependencyIssue.wantedPeer.name}@${opts.peerDependencyIssue.wantedPeer.range} but none was installed.`
|
||||
}
|
||||
|
||||
function packageFriendlyId (manifest: {name: string, version: string}) {
|
||||
return `${manifest.name}@${manifest.version}`
|
||||
}
|
||||
|
||||
function locationToFriendlyPath (location: PeerDependencyIssueLocation) {
|
||||
const result = location.parents.map(({ name }) => name)
|
||||
if (location.projectPath) {
|
||||
result.unshift(location.projectPath)
|
||||
}
|
||||
return result.join(' > ')
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import resolveDependencies from '@pnpm/resolve-dependencies'
|
||||
import getWantedDependencies from '@pnpm/resolve-dependencies/lib/getWantedDependencies'
|
||||
import { PeerDependencyIssues } from '@pnpm/types'
|
||||
import getContext, { GetContextOptions, ProjectOptions } from '@pnpm/get-context'
|
||||
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'
|
||||
@@ -19,7 +21,7 @@ export type ListMissingPeersOptions = Partial<GetContextOptions>
|
||||
>
|
||||
& Pick<GetContextOptions, 'storeDir'>
|
||||
|
||||
export async function listMissingPeers (
|
||||
export async function listPeerDependencyIssues (
|
||||
projects: ProjectOptions[],
|
||||
opts: ListMissingPeersOptions
|
||||
) {
|
||||
@@ -76,7 +78,34 @@ export async function listMissingPeers (
|
||||
}
|
||||
)
|
||||
|
||||
const conflicts = getPeerDependencyConflicts(peerDependencyIssues)
|
||||
|
||||
await waitTillAllFetchingsFinish()
|
||||
|
||||
return peerDependencyIssues
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
install,
|
||||
MutatedProject,
|
||||
mutateModules,
|
||||
PeerDependencyIssuesError,
|
||||
} from '@pnpm/core'
|
||||
import rimraf from '@zkochan/rimraf'
|
||||
import exists from 'path-exists'
|
||||
@@ -160,11 +161,25 @@ test('warning is reported when cannot resolve peer dependency for top-level depe
|
||||
|
||||
await addDependenciesToPackage({}, ['ajv-keywords@1.5.0'], await testDefaults({ reporter }))
|
||||
|
||||
expect(reporter).toBeCalledWith(
|
||||
expect(reporter).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
level: 'warn',
|
||||
name: 'pnpm',
|
||||
message: 'ajv-keywords@1.5.0 requires a peer of ajv@>=4.10.0 but none was installed.',
|
||||
level: 'debug',
|
||||
name: 'pnpm:peer-dependency-issues',
|
||||
bad: {},
|
||||
missing: {
|
||||
ajv: [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
parents: [
|
||||
{
|
||||
name: 'ajv-keywords',
|
||||
version: '1.5.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
wantedRange: '>=4.10.0',
|
||||
}],
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -172,11 +187,30 @@ test('warning is reported when cannot resolve peer dependency for top-level depe
|
||||
test('strict-peer-dependencies: error is thrown when cannot resolve peer dependency for top-level dependency', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
const reporter = sinon.spy()
|
||||
let err!: PeerDependencyIssuesError
|
||||
try {
|
||||
await addDependenciesToPackage({}, ['ajv-keywords@1.5.0'], await testDefaults({ strictPeerDependencies: true }))
|
||||
} catch (_err: any) { // eslint-disable-line
|
||||
err = _err
|
||||
}
|
||||
|
||||
await expect(
|
||||
addDependenciesToPackage({}, ['ajv-keywords@1.5.0'], await testDefaults({ reporter, strictPeerDependencies: true }))
|
||||
).rejects.toThrow(/ajv-keywords@1.5.0 requires a peer of ajv@>=4.10.0 but none was installed./)
|
||||
expect(err?.issues).toStrictEqual({
|
||||
bad: {},
|
||||
missing: {
|
||||
ajv: [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
parents: [
|
||||
{
|
||||
name: 'ajv-keywords',
|
||||
version: '1.5.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
wantedRange: '>=4.10.0',
|
||||
}],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('peer dependency is resolved from the dependencies of the workspace root project', async () => {
|
||||
@@ -220,9 +254,9 @@ test('peer dependency is resolved from the dependencies of the workspace root pr
|
||||
},
|
||||
], await testDefaults({ reporter }))
|
||||
|
||||
expect(reporter).not.toHaveBeenCalledWith({
|
||||
message: 'ajv-keywords@1.5.0 requires a peer of ajv@>=4.10.0 but none was installed.',
|
||||
})
|
||||
expect(reporter).not.toHaveBeenCalledWith(expect.objectContaining({
|
||||
name: 'pnpm:peer-dependency-issues',
|
||||
}))
|
||||
|
||||
{
|
||||
const lockfile = await projects.root.readLockfile()
|
||||
@@ -269,41 +303,118 @@ test('warning is reported when cannot resolve peer dependency for non-top-level
|
||||
prepareEmpty()
|
||||
await addDistTag({ package: 'abc-parent-with-ab', version: '1.0.0', distTag: 'latest' })
|
||||
|
||||
const reporter = sinon.spy()
|
||||
const reporter = jest.fn()
|
||||
|
||||
await addDependenciesToPackage({}, ['abc-grand-parent-without-c'], await testDefaults({ reporter }))
|
||||
|
||||
const logMatcher = sinon.match({
|
||||
message: 'abc-grand-parent-without-c > abc-parent-with-ab: abc@1.0.0 requires a peer of peer-c@^1.0.0 but none was installed.',
|
||||
})
|
||||
const reportedTimes = reporter.withArgs(logMatcher).callCount
|
||||
|
||||
expect(reportedTimes).toBe(1)
|
||||
expect(reporter).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
level: 'debug',
|
||||
name: 'pnpm:peer-dependency-issues',
|
||||
bad: {},
|
||||
missing: {
|
||||
'peer-c': [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
parents: [
|
||||
{
|
||||
name: 'abc-grand-parent-without-c',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'abc-parent-with-ab',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'abc',
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
wantedRange: '^1.0.0',
|
||||
}],
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('warning is reported when bad version of resolved peer dependency for non-top-level dependency', async () => {
|
||||
await addDistTag({ package: 'abc-parent-with-ab', version: '1.0.0', distTag: 'latest' })
|
||||
prepareEmpty()
|
||||
|
||||
const reporter = sinon.spy()
|
||||
const reporter = jest.fn()
|
||||
|
||||
await addDependenciesToPackage({}, ['abc-grand-parent-without-c', 'peer-c@2'], await testDefaults({ reporter }))
|
||||
|
||||
const logMatcher = sinon.match({
|
||||
message: 'abc-grand-parent-without-c > abc-parent-with-ab: abc@1.0.0 requires a peer of peer-c@^1.0.0 but version 2.0.0 was installed.',
|
||||
})
|
||||
const reportedTimes = reporter.withArgs(logMatcher).callCount
|
||||
|
||||
expect(reportedTimes).toBe(1)
|
||||
expect(reporter).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
level: 'debug',
|
||||
name: 'pnpm:peer-dependency-issues',
|
||||
bad: {
|
||||
'peer-c': [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
parents: [
|
||||
{
|
||||
name: 'abc-grand-parent-without-c',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'abc-parent-with-ab',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'abc',
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
foundVersion: '2.0.0',
|
||||
wantedRange: '^1.0.0',
|
||||
}],
|
||||
},
|
||||
missing: {},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('strict-peer-dependencies: error is thrown when bad version of resolved peer dependency for non-top-level dependency', async () => {
|
||||
await addDistTag({ package: 'abc-parent-with-ab', version: '1.0.0', distTag: 'latest' })
|
||||
prepareEmpty()
|
||||
|
||||
const reporter = sinon.spy()
|
||||
let err!: PeerDependencyIssuesError
|
||||
try {
|
||||
await addDependenciesToPackage({}, ['abc-grand-parent-without-c', 'peer-c@2'], await testDefaults({ strictPeerDependencies: true }))
|
||||
} catch (_err: any) { // eslint-disable-line
|
||||
err = _err
|
||||
}
|
||||
|
||||
await expect(
|
||||
addDependenciesToPackage({}, ['abc-grand-parent-without-c', 'peer-c@2'], await testDefaults({ reporter, strictPeerDependencies: true }))
|
||||
).rejects.toThrow('abc-grand-parent-without-c > abc-parent-with-ab: abc@1.0.0 requires a peer of peer-c@^1.0.0 but version 2.0.0 was installed.')
|
||||
expect(err?.issues).toStrictEqual({
|
||||
bad: {
|
||||
'peer-c': [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
parents: [
|
||||
{
|
||||
name: 'abc-grand-parent-without-c',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'abc-parent-with-ab',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'abc',
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
foundVersion: '2.0.0',
|
||||
wantedRange: '^1.0.0',
|
||||
}],
|
||||
},
|
||||
missing: {},
|
||||
})
|
||||
})
|
||||
|
||||
test('top peer dependency is linked on subsequent install', async () => {
|
||||
@@ -865,36 +976,45 @@ test('peer dependency is saved', async () => {
|
||||
test('warning is not reported when cannot resolve optional peer dependency', async () => {
|
||||
const project = prepareEmpty()
|
||||
|
||||
const reporter = sinon.spy()
|
||||
const reporter = jest.fn()
|
||||
|
||||
await addDependenciesToPackage({}, ['abc-optional-peers@1.0.0', 'peer-c@2.0.0'], await testDefaults({ reporter }))
|
||||
|
||||
{
|
||||
const logMatcher = sinon.match({
|
||||
message: 'abc-optional-peers@1.0.0 requires a peer of peer-a@^1.0.0 but none was installed.',
|
||||
expect(reporter).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
level: 'debug',
|
||||
name: 'pnpm:peer-dependency-issues',
|
||||
bad: {
|
||||
'peer-c': [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
parents: [
|
||||
{
|
||||
name: 'abc-optional-peers',
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
foundVersion: '2.0.0',
|
||||
wantedRange: '^1.0.0',
|
||||
}],
|
||||
},
|
||||
missing: {
|
||||
'peer-a': [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
parents: [
|
||||
{
|
||||
name: 'abc-optional-peers',
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
wantedRange: '^1.0.0',
|
||||
}],
|
||||
},
|
||||
})
|
||||
const reportedTimes = reporter.withArgs(logMatcher).callCount
|
||||
|
||||
expect(reportedTimes).toBe(1)
|
||||
}
|
||||
|
||||
{
|
||||
const logMatcher = sinon.match({
|
||||
message: 'abc-optional-peers@1.0.0 requires a peer of peer-b@^1.0.0 but none was installed.',
|
||||
})
|
||||
const reportedTimes = reporter.withArgs(logMatcher).callCount
|
||||
|
||||
expect(reportedTimes).toBe(0)
|
||||
}
|
||||
|
||||
{
|
||||
const logMatcher = sinon.match({
|
||||
message: 'abc-optional-peers@1.0.0 requires a peer of peer-c@^1.0.0 but version 2.0.0 was installed.',
|
||||
})
|
||||
const reportedTimes = reporter.withArgs(logMatcher).callCount
|
||||
|
||||
expect(reportedTimes).toBe(1)
|
||||
}
|
||||
)
|
||||
|
||||
const lockfile = await project.readLockfile()
|
||||
|
||||
@@ -911,27 +1031,31 @@ test('warning is not reported when cannot resolve optional peer dependency', asy
|
||||
test('warning is not reported when cannot resolve optional peer dependency (specified by meta field only)', async () => {
|
||||
const project = prepareEmpty()
|
||||
|
||||
const reporter = sinon.spy()
|
||||
const reporter = jest.fn()
|
||||
|
||||
await addDependenciesToPackage({}, ['abc-optional-peers-meta-only@1.0.0', 'peer-c@2.0.0'], await testDefaults({ reporter }))
|
||||
|
||||
{
|
||||
const logMatcher = sinon.match({
|
||||
message: 'abc-optional-peers-meta-only@1.0.0 requires a peer of peer-a@^1.0.0 but none was installed.',
|
||||
expect(reporter).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
level: 'debug',
|
||||
name: 'pnpm:peer-dependency-issues',
|
||||
bad: {},
|
||||
missing: {
|
||||
'peer-a': [{
|
||||
location: {
|
||||
projectPath: '',
|
||||
parents: [
|
||||
{
|
||||
name: 'abc-optional-peers-meta-only',
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
wantedRange: '^1.0.0',
|
||||
}],
|
||||
},
|
||||
})
|
||||
const reportedTimes = reporter.withArgs(logMatcher).callCount
|
||||
|
||||
expect(reportedTimes).toBe(1)
|
||||
}
|
||||
|
||||
{
|
||||
const logMatcher = sinon.match({
|
||||
message: 'abc-optional-peers-meta-only@1.0.0 requires a peer of peer-b@^1.0.0 but none was installed.',
|
||||
})
|
||||
const reportedTimes = reporter.withArgs(logMatcher).callCount
|
||||
|
||||
expect(reportedTimes).toBe(0)
|
||||
}
|
||||
)
|
||||
|
||||
const lockfile = await project.readLockfile()
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { listMissingPeers } 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 listMissingPeers([
|
||||
{
|
||||
manifest: {
|
||||
dependencies: {
|
||||
'ajv-keywords': '1.5.0',
|
||||
},
|
||||
},
|
||||
rootDir: process.cwd(),
|
||||
},
|
||||
], await testDefaults())
|
||||
|
||||
expect(peerDependencyIssues.length).toBe(1)
|
||||
})
|
||||
38
packages/core/test/listPeerDependencyIssues.test.ts
Normal file
38
packages/core/test/listPeerDependencyIssues.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { listPeerDependencyIssues } 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([
|
||||
{
|
||||
manifest: {
|
||||
dependencies: {
|
||||
'ajv-keywords': '1.5.0',
|
||||
},
|
||||
},
|
||||
rootDir: process.cwd(),
|
||||
},
|
||||
], await testDefaults())
|
||||
|
||||
expect(peerDependencyIssues.issues.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([
|
||||
{
|
||||
manifest: {
|
||||
dependencies: {
|
||||
'has-foo100-peer': '1.0.0',
|
||||
'has-foo101-peer': '1.0.0',
|
||||
},
|
||||
},
|
||||
rootDir: process.cwd(),
|
||||
},
|
||||
], await testDefaults())
|
||||
|
||||
expect(peerDependencyIssues.conflicts.length).toBe(1)
|
||||
})
|
||||
@@ -33,6 +33,7 @@
|
||||
"@pnpm/config": "workspace:13.6.1",
|
||||
"@pnpm/core-loggers": "workspace:6.0.6",
|
||||
"@pnpm/error": "workspace:2.0.0",
|
||||
"@pnpm/render-peer-issues": "workspace:0.0.0",
|
||||
"@pnpm/types": "workspace:7.6.0",
|
||||
"ansi-diff": "^1.1.1",
|
||||
"boxen": "^5.0.0",
|
||||
|
||||
@@ -102,6 +102,7 @@ export function toOutput$ (
|
||||
const registryPushStream = new Rx.Subject<logs.RegistryLog>()
|
||||
const rootPushStream = new Rx.Subject<logs.RootLog>()
|
||||
const packageManifestPushStream = new Rx.Subject<logs.PackageManifestLog>()
|
||||
const peerDependencyIssuesPushStream = new Rx.Subject<logs.PeerDependencyIssuesLog>()
|
||||
const linkPushStream = new Rx.Subject<logs.LinkLog>()
|
||||
const otherPushStream = new Rx.Subject<logs.Log>()
|
||||
const hookPushStream = new Rx.Subject<logs.HookLog>()
|
||||
@@ -139,6 +140,9 @@ export function toOutput$ (
|
||||
case 'pnpm:package-import-method':
|
||||
packageImportMethodPushStream.next(log)
|
||||
break
|
||||
case 'pnpm:peer-dependency-issues':
|
||||
peerDependencyIssuesPushStream.next(log)
|
||||
break
|
||||
case 'pnpm:install-check':
|
||||
installCheckPushStream.next(log)
|
||||
break
|
||||
@@ -193,6 +197,7 @@ export function toOutput$ (
|
||||
other,
|
||||
packageImportMethod: Rx.from(packageImportMethodPushStream),
|
||||
packageManifest: Rx.from(packageManifestPushStream),
|
||||
peerDependencyIssues: Rx.from(peerDependencyIssuesPushStream),
|
||||
progress: Rx.from(progressPushStream),
|
||||
registry: Rx.from(registryPushStream),
|
||||
requestRetry: Rx.from(requestRetryPushStream),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Config } from '@pnpm/config'
|
||||
import { Log } from '@pnpm/core-loggers'
|
||||
import PnpmError from '@pnpm/error'
|
||||
import renderPeerIssues from '@pnpm/render-peer-issues'
|
||||
import { PeerDependencyIssues } from '@pnpm/types'
|
||||
import chalk from 'chalk'
|
||||
import equals from 'ramda/src/equals'
|
||||
import StackTracey from 'stacktracey'
|
||||
@@ -55,6 +57,8 @@ function getErrorInfo (logObj: Log, config?: Config): {
|
||||
return reportLifecycleError(logObj as any) // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
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) // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
case 'ERR_PNPM_FETCH_401':
|
||||
case 'ERR_PNPM_FETCH_403':
|
||||
return reportAuthError(err, logObj as any, config) // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
@@ -388,3 +392,13 @@ function hideSecureInfo (key: string, value: string) {
|
||||
if (key.endsWith('_auth') || key.endsWith('_authToken')) return `${value.substring(0, 4)}[hidden]`
|
||||
return value
|
||||
}
|
||||
|
||||
function reportPeerDependencyIssuesError (
|
||||
err: Error,
|
||||
msg: { issues: PeerDependencyIssues }
|
||||
) {
|
||||
return {
|
||||
title: err.message,
|
||||
body: renderPeerIssues(msg.issues),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import reportHooks from './reportHooks'
|
||||
import reportInstallChecks from './reportInstallChecks'
|
||||
import reportLifecycleScripts from './reportLifecycleScripts'
|
||||
import reportMisc from './reportMisc'
|
||||
import reportPeerDependencyIssues from './reportPeerDependencyIssues'
|
||||
import reportProgress from './reportProgress'
|
||||
import reportRequestRetry from './reportRequestRetry'
|
||||
import reportScope from './reportScope'
|
||||
@@ -32,6 +33,7 @@ export default function (
|
||||
registry: Rx.Observable<logs.RegistryLog>
|
||||
root: Rx.Observable<logs.RootLog>
|
||||
packageManifest: Rx.Observable<logs.PackageManifestLog>
|
||||
peerDependencyIssues: Rx.Observable<logs.PeerDependencyIssuesLog>
|
||||
requestRetry: Rx.Observable<logs.RequestRetryLog>
|
||||
link: Rx.Observable<logs.LinkLog>
|
||||
other: Rx.Observable<logs.Log>
|
||||
@@ -64,6 +66,7 @@ export default function (
|
||||
cwd,
|
||||
throttle,
|
||||
}),
|
||||
reportPeerDependencyIssues(log$),
|
||||
reportLifecycleScripts(log$, {
|
||||
appendOnly: opts.appendOnly === true || opts.streamLifecycleOutput,
|
||||
cwd,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { PeerDependencyIssuesLog } from '@pnpm/core-loggers'
|
||||
import renderPeerIssues from '@pnpm/render-peer-issues'
|
||||
import * as Rx from 'rxjs'
|
||||
import { map, take } from 'rxjs/operators'
|
||||
import formatWarn from './utils/formatWarn'
|
||||
|
||||
export default (
|
||||
log$: {
|
||||
peerDependencyIssues: Rx.Observable<PeerDependencyIssuesLog>
|
||||
}
|
||||
) => {
|
||||
return log$.peerDependencyIssues.pipe(
|
||||
take(1),
|
||||
map((log) => Rx.of({
|
||||
msg: `${formatWarn('Issues with peer dependencies found')}\n${renderPeerIssues(log)}`,
|
||||
}))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { peerDependencyIssuesLogger } from '@pnpm/core-loggers'
|
||||
import { toOutput$ } from '@pnpm/default-reporter'
|
||||
import logger, {
|
||||
createStreamParser,
|
||||
} from '@pnpm/logger'
|
||||
import { take } from 'rxjs/operators'
|
||||
|
||||
test('print peer dependency issues warning', (done) => {
|
||||
const output$ = toOutput$({
|
||||
context: {
|
||||
argv: ['install'],
|
||||
},
|
||||
streamParser: createStreamParser(),
|
||||
})
|
||||
|
||||
peerDependencyIssuesLogger.debug({
|
||||
missing: {},
|
||||
bad: {
|
||||
a: [
|
||||
{
|
||||
location: {
|
||||
parents: [
|
||||
{
|
||||
name: 'b',
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
projectPath: '',
|
||||
},
|
||||
foundVersion: '2',
|
||||
wantedRange: '3',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect.assertions(1)
|
||||
|
||||
output$.pipe(take(1)).subscribe({
|
||||
complete: () => done(),
|
||||
error: done,
|
||||
next: output => {
|
||||
expect(output).toContain('<ROOT>')
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('print peer dependency issues error', (done) => {
|
||||
const output$ = toOutput$({
|
||||
context: { argv: ['install'] },
|
||||
streamParser: createStreamParser(),
|
||||
})
|
||||
|
||||
const err = new Error('some error')
|
||||
err['code'] = 'ERR_PNPM_PEER_DEP_ISSUES'
|
||||
err['issues'] = {
|
||||
missing: {},
|
||||
bad: {
|
||||
a: [
|
||||
{
|
||||
location: {
|
||||
parents: [
|
||||
{
|
||||
name: 'b',
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
projectPath: '',
|
||||
},
|
||||
wantedRange: '3',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
logger.error(err, err)
|
||||
|
||||
expect.assertions(1)
|
||||
|
||||
expect.assertions(1)
|
||||
|
||||
output$.pipe(take(1)).subscribe({
|
||||
complete: () => done(),
|
||||
error: done,
|
||||
next: output => {
|
||||
expect(output).toContain('<ROOT>')
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -18,6 +18,9 @@
|
||||
{
|
||||
"path": "../error"
|
||||
},
|
||||
{
|
||||
"path": "../render-peer-issues"
|
||||
},
|
||||
{
|
||||
"path": "../types"
|
||||
}
|
||||
|
||||
13
packages/render-peer-issues/README.md
Normal file
13
packages/render-peer-issues/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# @pnpm/render-peer-issues
|
||||
|
||||
> Visualizes peer dependency issues
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
pnpm add @pnpm/render-peer-issues
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
1
packages/render-peer-issues/jest.config.js
Normal file
1
packages/render-peer-issues/jest.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../../jest.config')
|
||||
40
packages/render-peer-issues/package.json
Normal file
40
packages/render-peer-issues/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@pnpm/render-peer-issues",
|
||||
"description": "Visualizes peer dependency issues",
|
||||
"version": "0.0.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pnpm/pnpm/issues"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"files": [
|
||||
"lib",
|
||||
"!*.map"
|
||||
],
|
||||
"keywords": [
|
||||
"pnpm6"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.17"
|
||||
},
|
||||
"repository": "https://github.com/pnpm/pnpm/blob/master/packages/render-peer-issues",
|
||||
"scripts": {
|
||||
"_test": "jest",
|
||||
"test": "pnpm run compile && pnpm run _test",
|
||||
"lint": "eslint src/**/*.ts test/**/*.ts",
|
||||
"prepublishOnly": "pnpm run compile",
|
||||
"compile": "rimraf lib tsconfig.tsbuildinfo && tsc --build && pnpm run lint -- --fix"
|
||||
},
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/master/packages/render-peer-issues#readme",
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
"dependencies": {
|
||||
"@pnpm/types": "workspace:7.6.0",
|
||||
"archy": "^1.0.0",
|
||||
"chalk": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/archy": "0.0.31",
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
}
|
||||
62
packages/render-peer-issues/src/index.ts
Normal file
62
packages/render-peer-issues/src/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { PeerDependencyIssues } from '@pnpm/types'
|
||||
import archy from 'archy'
|
||||
import chalk from 'chalk'
|
||||
|
||||
const ROOT_LABEL = '<ROOT>'
|
||||
|
||||
export default function (peerDependencyIssues: PeerDependencyIssues) {
|
||||
const projects = {} as Record<string, PkgNode>
|
||||
for (const [peerName, issues] of Object.entries(peerDependencyIssues.missing)) {
|
||||
for (const issue of issues) {
|
||||
const projectPath = issue.location.projectPath || ROOT_LABEL
|
||||
if (!projects[projectPath]) {
|
||||
projects[projectPath] = { dependencies: {}, peerIssues: [] }
|
||||
}
|
||||
createTree(projects[projectPath], issue.location.parents, `${chalk.red('✕ missing peer')} ${peerName}@"${issue.wantedRange}"`)
|
||||
}
|
||||
}
|
||||
for (const [peerName, issues] of Object.entries(peerDependencyIssues.bad)) {
|
||||
for (const issue of issues) {
|
||||
const projectPath = issue.location.projectPath || ROOT_LABEL
|
||||
if (!projects[projectPath]) {
|
||||
projects[projectPath] = { dependencies: {}, peerIssues: [] }
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
createTree(projects[projectPath], 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('')
|
||||
}
|
||||
|
||||
interface PkgNode {
|
||||
peerIssues: string[]
|
||||
dependencies: Record<string, PkgNode>
|
||||
}
|
||||
|
||||
function createTree (pkgNode: PkgNode, pkgs: Array<{ name: string, version: string }>, issueText: string) {
|
||||
const [pkg, ...rest] = pkgs
|
||||
if (!pkgNode.dependencies[pkg.name]) {
|
||||
pkgNode.dependencies[pkg.name] = { dependencies: {}, peerIssues: [] }
|
||||
}
|
||||
if (rest.length === 0) {
|
||||
pkgNode.dependencies[pkg.name].peerIssues.push(issueText)
|
||||
return
|
||||
}
|
||||
createTree(pkgNode.dependencies[pkg.name], rest, issueText)
|
||||
}
|
||||
|
||||
function toArchyData (depName: string, pkgNode: PkgNode): archy.Data {
|
||||
const result: Required<archy.Data> = {
|
||||
label: depName,
|
||||
nodes: [],
|
||||
}
|
||||
for (const wantedPeer of pkgNode.peerIssues) {
|
||||
result.nodes.push(wantedPeer)
|
||||
}
|
||||
for (const [depName, node] of Object.entries(pkgNode.dependencies)) {
|
||||
result.nodes.push(toArchyData(depName, node))
|
||||
}
|
||||
return result
|
||||
}
|
||||
14
packages/render-peer-issues/test/__snapshots__/index.ts.snap
Normal file
14
packages/render-peer-issues/test/__snapshots__/index.ts.snap
Normal file
@@ -0,0 +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
|
||||
"
|
||||
`;
|
||||
77
packages/render-peer-issues/test/index.ts
Normal file
77
packages/render-peer-issues/test/index.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import renderPeerIssues from '@pnpm/render-peer-issues'
|
||||
import stripAnsi from 'strip-ansi'
|
||||
|
||||
test('renderPeerIssues()', () => {
|
||||
expect(stripAnsi(renderPeerIssues({
|
||||
missing: {
|
||||
aaa: [
|
||||
{
|
||||
location: {
|
||||
parents: [
|
||||
{
|
||||
name: 'xxx',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'yyy',
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
projectPath: '',
|
||||
},
|
||||
wantedRange: '^1.0.0',
|
||||
},
|
||||
],
|
||||
ddd: [
|
||||
{
|
||||
location: {
|
||||
parents: [
|
||||
{
|
||||
name: 'zzz',
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
projectPath: '/packages/0',
|
||||
},
|
||||
wantedRange: '^1.0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
bad: {
|
||||
bbb: [
|
||||
{
|
||||
location: {
|
||||
parents: [
|
||||
{
|
||||
name: 'xxx',
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
projectPath: '',
|
||||
},
|
||||
foundVersion: '2',
|
||||
wantedRange: '^1.0.0',
|
||||
},
|
||||
],
|
||||
ccc: [
|
||||
{
|
||||
location: {
|
||||
parents: [
|
||||
{
|
||||
name: 'xxx',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'yyy',
|
||||
version: '1.0.0',
|
||||
},
|
||||
],
|
||||
projectPath: '',
|
||||
},
|
||||
foundVersion: '2',
|
||||
wantedRange: '^1.0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
}))).toMatchSnapshot()
|
||||
})
|
||||
16
packages/render-peer-issues/tsconfig.json
Normal file
16
packages/render-peer-issues/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../typings/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../types"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
packages/render-peer-issues/tsconfig.lint.json
Normal file
8
packages/render-peer-issues/tsconfig.lint.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../typings/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -31,8 +31,6 @@ import resolveDependencyTree, {
|
||||
import resolvePeers, {
|
||||
GenericDependenciesGraph,
|
||||
GenericDependenciesGraphNode,
|
||||
PeerDependencyIssue,
|
||||
PeerDependencyIssueLocation,
|
||||
} from './resolvePeers'
|
||||
import toResolveImporter from './toResolveImporter'
|
||||
import updateLockfile from './updateLockfile'
|
||||
@@ -44,8 +42,6 @@ export type DependenciesGraphNode = GenericDependenciesGraphNode & ResolvedPacka
|
||||
|
||||
export {
|
||||
LinkedDependency,
|
||||
PeerDependencyIssue,
|
||||
PeerDependencyIssueLocation,
|
||||
ResolvedPackage,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import path from 'path'
|
||||
import { Dependencies } from '@pnpm/types'
|
||||
import { Dependencies, PeerDependencyIssues } from '@pnpm/types'
|
||||
import { depPathToFilename } from 'dependency-path'
|
||||
import { KeyValuePair } from 'ramda'
|
||||
import fromPairs from 'ramda/src/fromPairs'
|
||||
@@ -15,22 +15,6 @@ import {
|
||||
} from './resolveDependencies'
|
||||
import { createNodeId, splitNodeId } from './nodeIdUtils'
|
||||
|
||||
export interface PeerDependencyIssueLocation {
|
||||
parents: Array<{ name: string, version: string }>
|
||||
projectPath: string
|
||||
}
|
||||
|
||||
export interface PeerDependencyIssue {
|
||||
location: PeerDependencyIssueLocation
|
||||
pkg: PartialResolvedPackage
|
||||
rootDir: string
|
||||
foundPeerVersion?: string
|
||||
wantedPeer: {
|
||||
name: string
|
||||
range: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface GenericDependenciesGraphNode {
|
||||
// at this point the version is really needed only for logging
|
||||
modules: string
|
||||
@@ -73,14 +57,17 @@ export default function<T extends PartialResolvedPackage> (
|
||||
): {
|
||||
dependenciesGraph: GenericDependenciesGraph<T>
|
||||
dependenciesByProjectId: {[id: string]: {[alias: string]: string}}
|
||||
peerDependencyIssues: PeerDependencyIssue[]
|
||||
peerDependencyIssues: PeerDependencyIssues
|
||||
} {
|
||||
const depGraph: GenericDependenciesGraph<T> = {}
|
||||
const pathsByNodeId = {}
|
||||
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: PeerDependencyIssue[] = []
|
||||
const peerDependencyIssues: PeerDependencyIssues = {
|
||||
bad: {},
|
||||
missing: {},
|
||||
}
|
||||
|
||||
for (const { directNodeIdsByAlias, topParents, rootDir } of opts.projects) {
|
||||
const pkgsByName = {
|
||||
@@ -172,7 +159,7 @@ function resolvePeersOfNode<T extends PartialResolvedPackage> (
|
||||
pathsByNodeId: {[nodeId: string]: string}
|
||||
depGraph: GenericDependenciesGraph<T>
|
||||
virtualStoreDir: string
|
||||
peerDependencyIssues: PeerDependencyIssue[]
|
||||
peerDependencyIssues: PeerDependencyIssues
|
||||
peersCache: PeersCache
|
||||
purePkgs: Set<string> // pure packages are those that don't rely on externally resolved peers
|
||||
rootDir: string
|
||||
@@ -352,7 +339,7 @@ function resolvePeersOfChildren<T extends PartialResolvedPackage> (
|
||||
parentPkgs: ParentRefs,
|
||||
ctx: {
|
||||
pathsByNodeId: {[nodeId: string]: string}
|
||||
peerDependencyIssues: PeerDependencyIssue[]
|
||||
peerDependencyIssues: PeerDependencyIssues
|
||||
peersCache: PeersCache
|
||||
virtualStoreDir: string
|
||||
purePkgs: Set<string>
|
||||
@@ -390,7 +377,7 @@ function resolvePeers<T extends PartialResolvedPackage> (
|
||||
resolvedPackage: T
|
||||
dependenciesTree: DependenciesTree<T>
|
||||
rootDir: string
|
||||
peerDependencyIssues: PeerDependencyIssue[]
|
||||
peerDependencyIssues: PeerDependencyIssues
|
||||
}
|
||||
): PeersResolution {
|
||||
const resolvedPeers: {[alias: string]: string} = {}
|
||||
@@ -407,38 +394,36 @@ function resolvePeers<T extends PartialResolvedPackage> (
|
||||
) {
|
||||
continue
|
||||
}
|
||||
ctx.peerDependencyIssues.push({
|
||||
if (!ctx.peerDependencyIssues.missing[peerName]) {
|
||||
ctx.peerDependencyIssues.missing[peerName] = []
|
||||
}
|
||||
ctx.peerDependencyIssues.missing[peerName].push({
|
||||
location: getLocationFromNodeId({
|
||||
dependenciesTree: ctx.dependenciesTree,
|
||||
nodeId: ctx.nodeId,
|
||||
lockfileDir: ctx.lockfileDir,
|
||||
rootDir: ctx.rootDir,
|
||||
pkg: ctx.resolvedPackage,
|
||||
}),
|
||||
pkg: ctx.resolvedPackage,
|
||||
rootDir: ctx.rootDir,
|
||||
wantedPeer: {
|
||||
name: peerName,
|
||||
range: peerVersionRange,
|
||||
},
|
||||
wantedRange: peerVersionRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (!semver.satisfies(resolved.version, peerVersionRange, { loose: true })) {
|
||||
ctx.peerDependencyIssues.push({
|
||||
if (!ctx.peerDependencyIssues.bad[peerName]) {
|
||||
ctx.peerDependencyIssues.bad[peerName] = []
|
||||
}
|
||||
ctx.peerDependencyIssues.bad[peerName].push({
|
||||
location: getLocationFromNodeId({
|
||||
dependenciesTree: ctx.dependenciesTree,
|
||||
nodeId: ctx.nodeId,
|
||||
lockfileDir: ctx.lockfileDir,
|
||||
rootDir: ctx.rootDir,
|
||||
pkg: ctx.resolvedPackage,
|
||||
}),
|
||||
pkg: ctx.resolvedPackage,
|
||||
rootDir: ctx.rootDir,
|
||||
foundPeerVersion: resolved.version,
|
||||
wantedPeer: {
|
||||
name: peerName,
|
||||
range: peerVersionRange,
|
||||
},
|
||||
foundVersion: resolved.version,
|
||||
wantedRange: peerVersionRange,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -453,10 +438,12 @@ function getLocationFromNodeId<T> (
|
||||
lockfileDir,
|
||||
nodeId,
|
||||
rootDir,
|
||||
pkg,
|
||||
}: {
|
||||
dependenciesTree: DependenciesTree<T>
|
||||
lockfileDir: string
|
||||
nodeId: string
|
||||
pkg: PartialResolvedPackage
|
||||
rootDir: string
|
||||
}
|
||||
) {
|
||||
@@ -464,6 +451,7 @@ function getLocationFromNodeId<T> (
|
||||
const parents = scan((prevNodeId, pkgId) => createNodeId(prevNodeId, pkgId), '>', parts)
|
||||
.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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './misc'
|
||||
export * from './options'
|
||||
export * from './package'
|
||||
export * from './peerDependencyIssues'
|
||||
export * from './project'
|
||||
|
||||
18
packages/types/src/peerDependencyIssues.ts
Normal file
18
packages/types/src/peerDependencyIssues.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface PeerDependencyIssueLocation {
|
||||
parents: Array<{ name: string, version: string }>
|
||||
projectPath: string
|
||||
}
|
||||
|
||||
export interface MissingPeerDependencyIssue {
|
||||
location: PeerDependencyIssueLocation
|
||||
wantedRange: string
|
||||
}
|
||||
|
||||
export interface BadPeerDependencyIssue extends MissingPeerDependencyIssue {
|
||||
foundVersion: string
|
||||
}
|
||||
|
||||
export interface PeerDependencyIssues {
|
||||
bad: Record<string, BadPeerDependencyIssue[]>
|
||||
missing: Record<string, MissingPeerDependencyIssue[]>
|
||||
}
|
||||
35
pnpm-lock.yaml
generated
35
pnpm-lock.yaml
generated
@@ -40,7 +40,7 @@ importers:
|
||||
'@commitlint/prompt-cli': ^15.0.0
|
||||
'@pnpm/eslint-config': workspace:*
|
||||
'@pnpm/meta-updater': 0.0.6
|
||||
'@pnpm/registry-mock': ^2.10.0
|
||||
'@pnpm/registry-mock': ^2.11.0
|
||||
'@pnpm/tsconfig': workspace:*
|
||||
'@types/jest': ^26.0.24
|
||||
'@types/node': ^14.17.32
|
||||
@@ -71,7 +71,7 @@ importers:
|
||||
'@commitlint/prompt-cli': 15.0.0
|
||||
'@pnpm/eslint-config': link:utils/eslint-config
|
||||
'@pnpm/meta-updater': 0.0.6
|
||||
'@pnpm/registry-mock': 2.10.0
|
||||
'@pnpm/registry-mock': 2.11.0
|
||||
'@pnpm/tsconfig': link:utils/tsconfig
|
||||
'@types/jest': 26.0.24
|
||||
'@types/node': 14.17.34
|
||||
@@ -449,6 +449,7 @@ 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
|
||||
@@ -502,6 +503,7 @@ 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
|
||||
@@ -556,6 +558,7 @@ importers:
|
||||
'@pnpm/default-reporter': 'link:'
|
||||
'@pnpm/error': workspace:2.0.0
|
||||
'@pnpm/logger': ^4.0.0
|
||||
'@pnpm/render-peer-issues': workspace:0.0.0
|
||||
'@pnpm/types': workspace:7.6.0
|
||||
'@types/normalize-path': ^3.0.0
|
||||
'@types/ramda': 0.27.39
|
||||
@@ -580,6 +583,7 @@ importers:
|
||||
'@pnpm/config': link:../config
|
||||
'@pnpm/core-loggers': link:../core-loggers
|
||||
'@pnpm/error': link:../error
|
||||
'@pnpm/render-peer-issues': link:../render-peer-issues
|
||||
'@pnpm/types': link:../types
|
||||
ansi-diff: 1.1.1
|
||||
boxen: 5.1.2
|
||||
@@ -3052,6 +3056,23 @@ importers:
|
||||
'@types/is-windows': 1.0.0
|
||||
'@types/ramda': 0.27.39
|
||||
|
||||
packages/render-peer-issues:
|
||||
specifiers:
|
||||
'@pnpm/render-peer-issues': 'link:'
|
||||
'@pnpm/types': workspace:7.6.0
|
||||
'@types/archy': 0.0.31
|
||||
archy: ^1.0.0
|
||||
chalk: ^4.1.0
|
||||
strip-ansi: ^6.0.0
|
||||
dependencies:
|
||||
'@pnpm/types': link:../types
|
||||
archy: 1.0.0
|
||||
chalk: 4.1.2
|
||||
devDependencies:
|
||||
'@pnpm/render-peer-issues': 'link:'
|
||||
'@types/archy': 0.0.31
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
packages/resolve-dependencies:
|
||||
specifiers:
|
||||
'@pnpm/constants': workspace:5.0.0
|
||||
@@ -4940,8 +4961,8 @@ packages:
|
||||
strip-bom: 4.0.0
|
||||
dev: true
|
||||
|
||||
/@pnpm/registry-mock/2.10.0:
|
||||
resolution: {integrity: sha512-96frA3W36rjd6BRIjKmkXKTLIlpZFogvkbKnYp68+K2RWBoqDQd789ZZbJvz6Tz3fNWYcKpz6lUI4Bm3S+jtwA==}
|
||||
/@pnpm/registry-mock/2.11.0:
|
||||
resolution: {integrity: sha512-260WRwYOBizASI/LMYT0gxYpfRCFOezBxu7gL3UlRSCaWnCMP3COegrCKFWinQBRnFSgkns7BV7kaRryg3V5wg==}
|
||||
engines: {node: '>=10.13'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
@@ -13932,6 +13953,12 @@ packages:
|
||||
xmlchars: 2.2.0
|
||||
dev: true
|
||||
|
||||
/semver-intersect/1.4.0:
|
||||
resolution: {integrity: sha512-d8fvGg5ycKAq0+I6nfWeCx6ffaWJCsBYU0H2Rq56+/zFePYfT8mXkB3tWBSjR5BerkHNZ5eTPIk1/LBYas35xQ==}
|
||||
dependencies:
|
||||
semver: 5.7.1
|
||||
dev: false
|
||||
|
||||
/semver-utils/1.1.4:
|
||||
resolution: {integrity: sha512-EjnoLE5OGmDAVV/8YDoN5KiajNadjzIp9BAHOhYeQHt7j0UWxjmgsx4YD48wp4Ue1Qogq38F1GNUJNqF1kKKxA==}
|
||||
dev: false
|
||||
|
||||
4
typings/typed.d.ts
vendored
4
typings/typed.d.ts
vendored
@@ -60,3 +60,7 @@ 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