Skip to content

Commit

Permalink
fix: Is there a way to not upgrade a package if it's peer depndencies…
Browse files Browse the repository at this point in the history
… are not currently met #1418
  • Loading branch information
rbnayax authored and raineorshine committed Aug 30, 2024
1 parent 42e2bc7 commit e432959
Show file tree
Hide file tree
Showing 6 changed files with 379 additions and 28 deletions.
51 changes: 33 additions & 18 deletions src/lib/getIgnoredUpgradesDueToPeerDeps.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { satisfies } from 'semver'
import { intersects, satisfies } from 'semver'
import { IgnoredUpgradeDueToPeerDeps } from '../types/IgnoredUpgradeDueToPeerDeps'
import { Index } from '../types/IndexType'
import { Options } from '../types/Options'
import { Version } from '../types/Version'
import { VersionSpec } from '../types/VersionSpec'
import getPeerDependenciesFromRegistry from './getPeerDependenciesFromRegistry'
import upgradePackageDefinitions from './upgradePackageDefinitions'

/** Get all upgrades that are ignored due to incompatible peer dependencies. */
Expand All @@ -13,36 +14,50 @@ export async function getIgnoredUpgradesDueToPeerDeps(
upgradedPeerDependencies: Index<Index<Version>>,
options: Options = {},
) {
const upgradedPackagesWithPeerRestriction = {
...current,
...upgraded,
}
const [upgradedLatestVersions, latestVersionResults] = await upgradePackageDefinitions(current, {
...options,
peer: false,
peerDependencies: undefined,
loglevel: 'silent',
})

const upgradedPeerDependenciesLatest = await getPeerDependenciesFromRegistry(upgradedLatestVersions, options)
return Object.entries(upgradedLatestVersions)
.filter(([pkgName, newVersion]) => upgraded[pkgName] !== newVersion)
.reduce(
(accum, [pkgName, newVersion]) => ({
.reduce((accum, [pkgName, newVersion]) => {
let reason = Object.entries(upgradedPeerDependencies)
.filter(
([, peers]) =>
peers[pkgName] !== undefined &&
latestVersionResults[pkgName]?.version &&
!satisfies(latestVersionResults[pkgName].version!, peers[pkgName]),
)
.reduce((accumReason, [peerPkg, peers]) => ({ ...accumReason, [peerPkg]: peers[pkgName] }), {} as Index<string>)
if (Object.keys(reason).length === 0) {
const peersOfPkg = upgradedPeerDependenciesLatest?.[pkgName] || {}
reason = Object.entries(peersOfPkg)
.filter(
([peer, peerSpec]) =>
upgradedPackagesWithPeerRestriction[peer] &&
!intersects(upgradedPackagesWithPeerRestriction[peer], peerSpec),
)
.reduce(
(accumReason, [peerPkg, peerSpec]) => ({ ...accumReason, [pkgName]: `${peerPkg} ${peerSpec}` }),
{} as Index<string>,
)
}
return {
...accum,
[pkgName]: {
from: current[pkgName],
to: newVersion,
reason: Object.entries(upgradedPeerDependencies)
.filter(
([, peers]) =>
peers[pkgName] !== undefined &&
latestVersionResults[pkgName]?.version &&
!satisfies(latestVersionResults[pkgName].version!, peers[pkgName]),
)
.reduce(
(accumReason, [peerPkg, peers]) => ({ ...accumReason, [peerPkg]: peers[pkgName] }),
{} as Index<string>,
),
reason,
},
}),
{} as Index<IgnoredUpgradeDueToPeerDeps>,
)
}
}, {} as Index<IgnoredUpgradeDueToPeerDeps>)
}

export default getIgnoredUpgradesDueToPeerDeps
116 changes: 106 additions & 10 deletions src/lib/upgradePackageDefinitions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { dequal } from 'dequal'
import { satisfies } from 'semver'
import { intersects, satisfies } from 'semver'
import { parse, parseRange } from 'semver-utils'
import { Index } from '../types/IndexType'
import { Options } from '../types/Options'
Expand All @@ -11,6 +11,56 @@ import { pickBy } from './pick'
import queryVersions from './queryVersions'
import upgradeDependencies from './upgradeDependencies'

type CheckIfInPeerViolationResult = {
violated: boolean
filteredUpgradedDependencies: Index<VersionSpec>
upgradedPeerDependencies: Index<Index<VersionSpec>>
}

/**
* Check if the peer dependencies constraints of each upgraded package, are in violation,
* thus rendering the upgrade to be invalid
*
* @returns Whether there was any violation, and the upgrades that are not in violation
*/
const checkIfInPeerViolation = (
currentDependencies: Index<VersionSpec>,
filteredUpgradedDependencies: Index<VersionSpec>,
upgradedPeerDependencies: Index<Index<VersionSpec>>,
): CheckIfInPeerViolationResult => {
const upgradedDependencies = { ...currentDependencies, ...filteredUpgradedDependencies }
const filteredUpgradedDependenciesAfterPeers = pickBy(filteredUpgradedDependencies, (spec, dep) => {
const peerDeps = upgradedPeerDependencies[dep]
if (!peerDeps) {
return true
}
return Object.entries(peerDeps).every(
([peer, peerSpec]) =>
upgradedDependencies[peer] === undefined || intersects(upgradedDependencies[peer], peerSpec),
)
})
const violated =
Object.keys(filteredUpgradedDependencies).length > Object.keys(filteredUpgradedDependenciesAfterPeers).length
let filteredUpgradedPeerDependencies = upgradedPeerDependencies
if (violated) {
filteredUpgradedPeerDependencies = pickBy(
upgradedPeerDependencies,
(spec, dep) => filteredUpgradedDependenciesAfterPeers[dep] || !filteredUpgradedDependencies[dep],
)
}
return {
violated,
filteredUpgradedDependencies: filteredUpgradedDependenciesAfterPeers,
upgradedPeerDependencies: filteredUpgradedPeerDependencies,
}
}

export type UpgradePackageDefinitionsResult = [
upgradedDependencies: Index<VersionSpec>,
latestVersionResults: Index<VersionResult>,
newPeerDependencies?: Index<Index<VersionSpec>>,
]

/**
* Returns a 3-tuple of upgradedDependencies, their latest versions and the resulting peer dependencies.
*
Expand All @@ -21,7 +71,7 @@ import upgradeDependencies from './upgradeDependencies'
export async function upgradePackageDefinitions(
currentDependencies: Index<VersionSpec>,
options: Options,
): Promise<[Index<VersionSpec>, Index<VersionResult>, Index<Index<VersionSpec>>?]> {
): Promise<UpgradePackageDefinitionsResult> {
const latestVersionResults = await queryVersions(currentDependencies, options)

const latestVersions = keyValueBy(latestVersionResults, (dep, result) =>
Expand All @@ -47,22 +97,68 @@ export async function upgradePackageDefinitions(

const filteredLatestDependencies = pickBy(latestVersions, (spec, dep) => filteredUpgradedDependencies[dep])

let result: UpgradePackageDefinitionsResult = [
filteredUpgradedDependencies,
latestVersionResults,
options.peerDependencies,
]

if (!options.peer || Object.keys(filteredLatestDependencies).length === 0) {
return result
}

if (options.peer && Object.keys(filteredLatestDependencies).length > 0) {
const upgradedPeerDependencies = await getPeerDependenciesFromRegistry(filteredLatestDependencies, options)
const peerDependencies = { ...options.peerDependencies, ...upgradedPeerDependencies }
if (!dequal(options.peerDependencies, peerDependencies)) {

let checkPeerViolationResult: CheckIfInPeerViolationResult

if (
dequal(options.peerDependencies, {
...options.peerDependencies,
...upgradedPeerDependencies,
})
) {
checkPeerViolationResult = checkIfInPeerViolation(
currentDependencies,
filteredUpgradedDependencies,
options.peerDependencies!,
)
if (!checkPeerViolationResult.violated) {
return result
}
} else {
checkPeerViolationResult = {
violated: false,
filteredUpgradedDependencies,
upgradedPeerDependencies,
}
}
let runCount = 0
do {
if (runCount++ > 6) {
throw new Error(`Stuck in a while loop. Please report an issue`)
}
const peerDependenciesAfterUpgrade = {
...options.peerDependencies,
...checkPeerViolationResult.upgradedPeerDependencies,
}
if (dequal(options.peerDependencies, peerDependenciesAfterUpgrade)) {
// We can't find anything to do, will not upgrade anything
return [{}, latestVersionResults, options.peerDependencies]
}
const [newUpgradedDependencies, newLatestVersions, newPeerDependencies] = await upgradePackageDefinitions(
{ ...currentDependencies, ...filteredUpgradedDependencies },
{ ...options, peerDependencies, loglevel: 'silent' },
{ ...currentDependencies, ...checkPeerViolationResult.filteredUpgradedDependencies },
{ ...options, peerDependencies: peerDependenciesAfterUpgrade, loglevel: 'silent' },
)
return [
{ ...filteredUpgradedDependencies, ...newUpgradedDependencies },
result = [
{ ...checkPeerViolationResult.filteredUpgradedDependencies, ...newUpgradedDependencies },
{ ...latestVersionResults, ...newLatestVersions },
newPeerDependencies,
]
}
checkPeerViolationResult = checkIfInPeerViolation(currentDependencies, result[0], result[2]!)
} while (checkPeerViolationResult.violated)
}
return [filteredUpgradedDependencies, latestVersionResults, options.peerDependencies]
return result
}

export default upgradePackageDefinitions
107 changes: 107 additions & 0 deletions test/getIgnoredUpgradesDueToPeerDeps.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import getIgnoredUpgradesDueToPeerDeps from '../src/lib/getIgnoredUpgradesDueToPeerDeps'
import { Packument } from '../src/types/Packument'
import chaiSetup from './helpers/chaiSetup'
import stubVersions from './helpers/stubVersions'

chaiSetup()

Expand Down Expand Up @@ -31,4 +33,109 @@ describe('getIgnoredUpgradesDueToPeerDeps', function () {
},
})
})
it('ignored peer after upgrade', async () => {
const stub = stubVersions({
'@vitest/ui': {
version: '1.6.0',
versions: {
'1.3.1': {
version: '1.3.1',
} as Packument,
'1.6.0': {
version: '1.6.0',
} as Packument,
},
},
vitest: {
version: '1.6.0',
versions: {
'1.3.1': {
version: '1.3.1',
} as Packument,
'1.6.0': {
version: '1.6.0',
} as Packument,
},
},
eslint: {
version: '9.0.0',
versions: {
'8.57.0': {
version: '8.57.0',
} as Packument,
'9.0.0': {
version: '9.0.0',
} as Packument,
},
},
'eslint-plugin-import': {
version: '2.29.1',
versions: {
'2.29.1': {
version: '2.29.1',
} as Packument,
},
},
'eslint-plugin-unused-imports': {
version: '4.0.0',
versions: {
'4.0.0': {
version: '4.0.0',
} as Packument,
'3.0.0': {
version: '3.0.0',
} as Packument,
},
},
})
const data = await getIgnoredUpgradesDueToPeerDeps(
{
'@vitest/ui': '1.3.1',
vitest: '1.3.1',
eslint: '8.57.0',
'eslint-plugin-import': '2.29.1',
'eslint-plugin-unused-imports': '3.0.0',
},
{
'@vitest/ui': '1.6.0',
vitest: '1.6.0',
},
{
'@vitest/ui': {
vitest: '1.6.0',
},
vitest: {
jsdom: '*',
'happy-dom': '*',
'@vitest/ui': '1.6.0',
'@types/node': '^18.0.0 || >=20.0.0',
'@vitest/browser': '1.6.0',
'@edge-runtime/vm': '*',
},
eslint: {},
'eslint-plugin-import': {
eslint: '^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8',
},
'eslint-plugin-unused-imports': {
'@typescript-eslint/eslint-plugin': '^6.0.0',
eslint: '^8.0.0',
},
},
{
target: packageName => {
return packageName === 'eslint-plugin-unused-imports' ? 'greatest' : 'minor'
},
},
)
data.should.deep.equal({
'eslint-plugin-unused-imports': {
from: '3.0.0',
reason: {
'eslint-plugin-unused-imports': 'eslint 9',
},
to: '4.0.0',
},
})
stub.restore()
})
})
Loading

0 comments on commit e432959

Please sign in to comment.