diff --git a/README.md b/README.md index 4cc74ea4..ad196b79 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ ncu "/^(?!react-).*$/" # windows --deep Run recursively in current working directory. Alias of (--packageFile '**/package.json'). --dep Check one or more sections of dependencies only: - prod, dev, peer, optional, bundle + dev, optional, peer, prod, bundle (comma-delimited). --deprecated Include deprecated packages. --doctor Iteratively installs upgrades and runs tests to @@ -172,6 +172,8 @@ ncu "/^(?!react-).*$/" # windows --packageFile Package file(s) location (default: ./package.json). -p, --packageManager npm, yarn (default: "npm") +--peer Check peer dependencies of installed packages + and filter updates to compatible versions. --pre Include -alpha, -beta, -rc. (default: 0; default with --newest and --greatest: 1). --prefix Current working directory of npm. diff --git a/lib/cli-options.js b/lib/cli-options.js index 0c793f0e..9b5bba01 100644 --- a/lib/cli-options.js +++ b/lib/cli-options.js @@ -33,6 +33,11 @@ other version numbers that are higher. Includes prereleases.`]) // store CLI options separately from bin file so that they can be used to build type definitions const cliOptions = [ + { + long: 'peer', + description: 'Check peer dependencies of installed packages and filter updates to compatible versions.', + type: 'boolean' + }, { long: 'color', description: 'Force color in terminal', @@ -67,7 +72,7 @@ const cliOptions = [ { long: 'dep', arg: 'value', - description: 'Check one or more sections of dependencies only: prod, dev, peer, optional, bundle (comma-delimited).' + description: 'Check one or more sections of dependencies only: dev, optional, peer, prod, bundle (comma-delimited).' }, { long: 'deprecated', diff --git a/lib/index.d.ts b/lib/index.d.ts index fb5a3e84..1d7f87dc 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -33,7 +33,7 @@ declare namespace ncu { deep?: boolean; /** - * Check one or more sections of dependencies only: prod, dev, peer, optional, bundle (comma-delimited). + * Check one or more sections of dependencies only: dev, optional, peer, prod, bundle (comma-delimited). */ dep?: string; @@ -140,6 +140,11 @@ declare namespace ncu { */ packageManager?: string; + /** + * Check peer dependencies of installed packages and filter updates to compatible versions. + */ + peer?: boolean; + /** * Include -alpha, -beta, -rc. (default: 0; default with --newest and --greatest: 1). */ diff --git a/lib/index.js b/lib/index.js index fc1d35a4..0ab40561 100644 --- a/lib/index.js +++ b/lib/index.js @@ -142,6 +142,10 @@ async function analyzeProjectDependencies(options, pkgData, pkgFile) { options.enginesNode = _.get(pkg, 'engines.node') } + if (options.peer) { + options.peerDependencies = getPeerDependencies(current, options) + } + print(options, '\nOptions:', 'verbose') print(options, sortOptions(options), 'verbose') @@ -224,6 +228,31 @@ async function analyzeProjectDependencies(options, pkgData, pkgFile) { return output } +/** Get peer dependencies from installed packages */ +function getPeerDependencies(current, options) { + const basePath = options.cwd || './' + return Object.keys(current).map(pkgName => { + const path = basePath + 'node_modules/' + pkgName + '/package.json' + try { + const pkgData = fs.readFileSync(path) + const pkg = jph.parse(pkgData) + return 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 + }, {}) +} + // // Program // @@ -535,4 +564,4 @@ function getNcurc({ configFileName, configFilePath, packageFile } = {}) { return result ? { ...result, args } : null } -module.exports = { run, getNcurc, ...vm } +module.exports = { run, getNcurc, getPeerDependencies, ...vm } diff --git a/lib/package-managers/npm.js b/lib/package-managers/npm.js index 8f9aeaa3..408f6789 100644 --- a/lib/package-managers/npm.js +++ b/lib/package-managers/npm.js @@ -168,12 +168,27 @@ function satisfiesNodeEngine(versionResult, nodeEngine) { return versionNodeEngine && semver.satisfies(minVersion, versionNodeEngine) } +/** + * Returns true if the peer dependencies requirement is satisfied or not specified for a given package version. + * + * @param versionResult Version object returned by pacote.packument. + * @param peerDependencies The list of peer dependencies. + * @returns True if the peer dependencies are satisfied or not specified. + */ +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)) +} + /** Returns a composite predicate that filters out deprecated, prerelease, and node engine incompatibilies from version objects returns by pacote.packument. */ function filterPredicate(options) { return _.overEvery([ options.deprecated ? null : o => !o.deprecated, options.pre ? null : o => !versionUtil.isPre(o.version), options.enginesNode ? o => satisfiesNodeEngine(o, options.enginesNode) : null, + options.peerDependencies ? o => satisfiesPeerDependencies(o, options.peerDependencies) : null, ]) } diff --git a/lib/versionmanager.js b/lib/versionmanager.js index 28ea0d44..516ca51c 100644 --- a/lib/versionmanager.js +++ b/lib/versionmanager.js @@ -295,25 +295,17 @@ async function upgradePackageData(pkgData, oldDependencies, newDependencies, new */ function getCurrentDependencies(pkgData = {}, options = {}) { - if (options.dep) { - const deps = (options.dep || '').split(',') - options.prod = deps.includes('prod') - options.dev = deps.includes('dev') - options.peer = deps.includes('peer') - options.optional = deps.includes('optional') - options.bundle = deps.includes('bundle') - } - else { - options.prod = options.dev = options.peer = options.optional = options.bundle = true - } + const deps = options.dep + ? (options.dep || '').split(',') + : ['dev', 'optional', 'peer', 'prod', 'bundle'] const allDependencies = cint.filterObject( { - ...options.prod && pkgData.dependencies, - ...options.dev && pkgData.devDependencies, - ...options.peer && pkgData.peerDependencies, - ...options.optional && pkgData.optionalDependencies, - ...options.bundle && pkgData.bundleDependencies + ...deps.includes('prod') && pkgData.dependencies, + ...deps.includes('dev') && pkgData.devDependencies, + ...deps.includes('peer') && pkgData.peerDependencies, + ...deps.includes('optional') && pkgData.optionalDependencies, + ...deps.includes('bundle') && pkgData.bundleDependencies }, filterAndReject(options.filter, options.reject, options.filterVersion, options.rejectVersion) ) diff --git a/test/index.test.js b/test/index.test.js index 982bb92b..b97121bb 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -2,10 +2,12 @@ const fs = require('fs') const path = require('path') +const rimraf = require('rimraf') const chai = require('chai') const chaiAsPromised = require('chai-as-promised') const chaiString = require('chai-string') const ncu = require('../lib/') +const { npm: spawnNpm } = require('../lib/package-managers/npm') chai.use(chaiAsPromised) chai.use(chaiString) @@ -708,4 +710,37 @@ describe('run', function () { }) + describe('peer dependencies', () => { + const peerPath = path.join(__dirname, '/peer/') + + it('peer dependencies of installed packages are ignored by default', async () => { + try { + await spawnNpm('install', {}, { cwd: peerPath }) + const upgrades = await ncu.run({ cwd: peerPath }) + upgrades.should.deep.equal({ + 'ncu-test-return-version': '2.0.0' + }) + } + finally { + rimraf.sync(path.join(peerPath, 'node_modules')) + rimraf.sync(path.join(peerPath, 'package-lock.json')) + } + }) + + it('peer dependencies of installed packages are checked when using option peer', async () => { + try { + await spawnNpm('install', {}, { cwd: peerPath }) + const upgrades = await ncu.run({ cwd: peerPath, peer: true }) + upgrades.should.deep.equal({ + 'ncu-test-return-version': '1.1.0' + }) + } + finally { + rimraf.sync(path.join(peerPath, 'node_modules')) + rimraf.sync(path.join(peerPath, 'package-lock.json')) + } + }) + + }) + }) diff --git a/test/peer/package.json b/test/peer/package.json new file mode 100644 index 00000000..d14722aa --- /dev/null +++ b/test/peer/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "ncu-test-peer": "1.0.0", + "ncu-test-return-version": "1.0.0" + } +}