Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update peer dependencies on upgrade and retry #874

Merged
merged 4 commits into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ ncu "/^(?!react-).*$/" # windows
./package.json).
-p, --packageManager <name> npm, yarn (default: "npm")
--peer Check peer dependencies of installed packages
and filter updates to compatible versions.
and filter updates to compatible versions. Run
"ncu --help --peer" for details.
--pre <n> Include -alpha, -beta, -rc. (default: 0; default
with --newest and --greatest: 1).
--prefix <path> Current working directory of npm.
Expand Down
41 changes: 39 additions & 2 deletions lib/cli-options.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const _ = require('lodash')
const Table = require('cli-table')
const chalk = require('chalk')
const { deepPatternPrefix } = require('./constants')

/*
Expand Down Expand Up @@ -35,8 +36,44 @@ other version numbers that are higher. Includes prereleases.`])
const cliOptions = [
{
long: 'peer',
description: 'Check peer dependencies of installed packages and filter updates to compatible versions.',
type: 'boolean'
description: 'Check peer dependencies of installed packages and filter updates to compatible versions. Run "ncu --help --peer" for details.',
type: 'boolean',
help: `Check peer dependencies of installed packages and filter updates to compatible versions.

${chalk.bold('Example')}

The following example demonstrates how --peer works, and how it uses peer dependencies from upgraded modules.

The package ${chalk.bold('ncu-test-peer-update')} has two versions published:

- 1.0.0 has peer dependency "ncu-test-return-version": "1.0.x"
- 1.1.0 has peer dependency "ncu-test-return-version": "1.1.x"

Our test app has the following dependencies:

"ncu-test-peer-update": "1.0.0",
"ncu-test-return-version": "1.0.0"

The latest versions of these packages are:

"ncu-test-peer-update": "1.1.0",
"ncu-test-return-version": "2.0.0"

${chalk.bold('With --peer')}

ncu upgrades packages to the highest version that still adheres to the peer dependency constraints:


ncu-test-peer-update 1.0.0 → 1.${chalk.cyan('1.0')}
ncu-test-return-version 1.0.0 → 1.${chalk.cyan('1.0')}

${chalk.bold('Without --peer')}

As a comparison: without using the --peer option, ncu will suggest the latest versions, ignoring peer dependencies:

ncu-test-peer-update 1.0.0 → 1.${chalk.cyan('1.0')}
ncu-test-return-version 1.0.0 → ${chalk.red('2.0.0')}
`
},
{
long: 'color',
Expand Down
2 changes: 1 addition & 1 deletion lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ declare namespace ncu {
packageManager?: string;

/**
* Check peer dependencies of installed packages and filter updates to compatible versions.
* Check peer dependencies of installed packages and filter updates to compatible versions. Run "ncu --help --peer" for details.
*/
peer?: boolean;

Expand Down
20 changes: 8 additions & 12 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,10 @@ async function analyzeProjectDependencies(options, pkgData, pkgFile) {
print(options, '\nOptions:', 'verbose')
print(options, sortOptions(options), 'verbose')

const [upgraded, latest] = await vm.upgradePackageDefinitions(current, options)
const [upgraded, latest, upgradedPeerDependencies] = await vm.upgradePackageDefinitions(current, options)

print(options, '\nupgradedPeerDependencies:', 'verbose')
print(options, upgradedPeerDependencies, 'verbose')

print(options, '\nFetched:', 'verbose')
print(options, latest, 'verbose')
Expand Down Expand Up @@ -231,25 +234,18 @@ async function analyzeProjectDependencies(options, pkgData, pkgFile) {
/** Get peer dependencies from installed packages */
function getPeerDependencies(current, options) {
const basePath = options.cwd || './'
return Object.keys(current).map(pkgName => {
return Object.keys(current).reduce((accum, pkgName) => {
const path = basePath + 'node_modules/' + pkgName + '/package.json'
let peers = {}
try {
const pkgData = fs.readFileSync(path)
const pkg = jph.parse(pkgData)
return vm.getCurrentDependencies(pkg, { ...options, dep: 'peer' })
peers = vm.getCurrentDependencies(pkg, { ...options, dep: 'peer' })
}
catch (e) {
print(options, 'Could not read peer dependencies for package ' + pkgName + '. Is this package installed?', 'warn')
return {}
}
}).reduce((acc, peers) => {
Object.entries(peers).forEach(([pkgName, version]) => {
if (acc[pkgName] === undefined) {
acc[pkgName] = []
}
acc[pkgName][acc[pkgName].length] = version
})
return acc
return { ...accum, [pkgName]: peers }
}, {})
}

Expand Down
25 changes: 20 additions & 5 deletions lib/package-managers/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,9 @@ function satisfiesNodeEngine(versionResult, nodeEngine) {
*/
function satisfiesPeerDependencies(versionResult, peerDependencies) {
if (!peerDependencies) return true
const pkgPeerDependencies = peerDependencies[versionResult.name]
if (!pkgPeerDependencies) return true
return pkgPeerDependencies.every(v => semver.satisfies(versionResult.version, v))
return Object.values(peerDependencies).every(
peers => peers[versionResult.name] === undefined || semver.satisfies(versionResult.version, peers[versionResult.name])
)
}

/** Returns a composite predicate that filters out deprecated, prerelease, and node engine incompatibilies from version objects returns by pacote.packument. */
Expand All @@ -202,9 +202,9 @@ function filterPredicate(options) {
*/
function spawnNpm(args, npmOptions = {}, spawnOptions = {}) {
const cmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'
args = Array.isArray(args) ? args : [args]

const fullArgs = [].concat(
args,
const fullArgs = args.concat(
npmOptions.global ? '--global' : [],
npmOptions.prefix ? `--prefix=${npmOptions.prefix}` : [],
'--depth=0',
Expand Down Expand Up @@ -256,6 +256,21 @@ module.exports = {

npm: spawnNpm,

/**
* Requests the list of peer dependencies for a specific package version
*
* @param packageName
* @param version
* @returns Promised {packageName: version} collection
*/
async getPeerDependencies(packageName, version) {
const result = await spawnNpm(
['view', packageName + '@' + version, 'peerDependencies'],
{},
{ rejectOnError: false })
return result ? parseJson(result, { command: 'npm view' }) : {}
},

/**
* @param [options]
* @param [options.cwd]
Expand Down
48 changes: 46 additions & 2 deletions lib/versionmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ async function getOwnerPerDependency(fromVersion, toVersion, options) {
}

/**
* Returns an 2-tuple of upgradedDependencies and their latest versions.
* Returns an 3-tuple of upgradedDependencies, their latest versions and the resulting peer dependencies.
*
* @param currentDependencies
* @param options
Expand All @@ -231,7 +231,22 @@ async function upgradePackageDefinitions(currentDependencies, options) {
return !options.jsonUpgraded || !options.minimal || !isSatisfied(latestVersions[dep], currentDependencies[dep])
})

return [filteredUpgradedDependencies, latestVersions]
if (options.peer && !_.isEmpty(filteredUpgradedDependencies)) {
const upgradedPeerDependencies = await getPeerDependenciesFromRegistry(filteredUpgradedDependencies, options)
const peerDependencies = { ...options.peerDependencies, ...upgradedPeerDependencies }
if (!_.isEqual(options.peerDependencies, peerDependencies)) {
const [newUpgradedDependencies, newLatestVersions, newPeerDependencies] = await upgradePackageDefinitions(
{ ...currentDependencies, ...filteredUpgradedDependencies },
{ ...options, peerDependencies, loglevel: 'silent' }
)
return [
{ ...filteredUpgradedDependencies, ...newUpgradedDependencies },
{ ...latestVersions, ...newLatestVersions },
newPeerDependencies
]
}
}
return [filteredUpgradedDependencies, latestVersions, options.peerDependencies]
}

/**
Expand Down Expand Up @@ -441,6 +456,34 @@ async function queryVersions(packageMap, options = {}) {
return _.pickBy(zipVersions(versions), _.identity)
}

/**
* Get the latest or greatest versions from the NPM repository based on the version target.
*
* @param packageMap An object whose keys are package name and values are version
* @param [options={}] Options.
* @returns Promised {packageName: peer dependencies} collection
*/
async function getPeerDependenciesFromRegistry(packageMap, options) {
const packageManager = getPackageManager(options.packageManager)
if (!packageManager.getPeerDependencies) return {}

const numItems = Object.keys(packageMap).length
let bar
if (!options.json && options.loglevel !== 'silent' && options.loglevel !== 'verbose' && numItems > 0) {
bar = new ProgressBar('[:bar] :current/:total :percent', { total: numItems, width: 20 })
bar.render()
}

return Object.entries(packageMap).reduce(async (accumPromise, [pkg, version]) => {
const dep = await packageManager.getPeerDependencies(pkg, version)
if (bar) {
bar.tick()
}
const accum = await accumPromise
return { ...accum, [pkg]: dep }
}, {})
}

/**
*
* @param dependencies A dependencies collection
Expand Down Expand Up @@ -532,4 +575,5 @@ module.exports = {
queryVersions,
upgradeDependencies,
getPackageManager,
getPeerDependenciesFromRegistry,
}
16 changes: 16 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,22 @@ describe('run', function () {
}
})

const peerUpdatePath = path.join(__dirname, '/peer-update/')
it('peer dependencies of installed packages are checked iteratively when using option peer', async () => {
try {
await spawnNpm('install', {}, { cwd: peerUpdatePath })
const upgrades = await ncu.run({ cwd: peerUpdatePath, peer: true })
upgrades.should.deep.equal({
'ncu-test-return-version': '1.1.0',
'ncu-test-peer-update': '1.1.0'
})
}
finally {
rimraf.sync(path.join(peerUpdatePath, 'node_modules'))
rimraf.sync(path.join(peerUpdatePath, 'package-lock.json'))
}
})

})

})
7 changes: 7 additions & 0 deletions test/package-managers/npm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ describe('npm', function () {
await packageManagers.npm.packageAuthorChanged('htmlparser2', '^3.10.1', '^4.0.0').should.eventually.equal(false)
await packageManagers.npm.packageAuthorChanged('ncu-test-v2', '^1.0.0', '2.2.0').should.eventually.be.null
})

it('getPeerDependencies', async () => {
await packageManagers.npm.getPeerDependencies('ncu-test-return-version', '1.0').should.eventually.deep.equal({})
await packageManagers.npm.getPeerDependencies('ncu-test-peer', '1.0').should.eventually.deep.equal({
'ncu-test-return-version': '1.x'
})
})
})
6 changes: 6 additions & 0 deletions test/peer-update/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"dependencies": {
"ncu-test-peer-update": "1.0.0",
"ncu-test-return-version": "1.0.0"
}
}
27 changes: 27 additions & 0 deletions test/versionmanager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,33 @@ describe('versionmanager', () => {
})
})

describe('getPeerDependenciesFromRegistry', function () {
it('single package', async () => {
const data = await vm.getPeerDependenciesFromRegistry({ 'ncu-test-peer': '1.0' }, {})
data.should.deep.equal({
'ncu-test-peer': {
'ncu-test-return-version': '1.x'
}
})
})
it('single package empty', async () => {
const data = await vm.getPeerDependenciesFromRegistry({ 'ncu-test-return-version': '1.0' }, {})
data.should.deep.equal({ 'ncu-test-return-version': {} })
})
it('multiple packages', async () => {
const data = await vm.getPeerDependenciesFromRegistry({
'ncu-test-return-version': '1.0.0',
'ncu-test-peer': '1.0.0',
}, {})
data.should.deep.equal({
'ncu-test-return-version': {},
'ncu-test-peer': {
'ncu-test-return-version': '1.x'
}
})
})
})

describe('queryVersions', function () {
// We increase the timeout to allow for more time to retrieve the version information
this.timeout(30000)
Expand Down