diff --git a/bin/ncu b/bin/ncu index bbaedb39..5476c709 100755 --- a/bin/ncu +++ b/bin/ncu @@ -23,6 +23,7 @@ program .option('--cwd ', 'Used as current working directory for `spawn` in npm listing') .option('--dep ', 'check only a specific section(s) of dependencies: prod|dev|peer|optional|bundle (comma-delimited)') .option('-e, --error-level ', 'set the error-level. 1: exits with error code 0 if no errors occur. 2: exits with error code 0 if no packages need updating (useful for continuous integration). Default is 1.', cint.partialAt(parseInt, 1, 10), 1) + .option('--engines-node', 'upgrade to version which satisfies engines.node range') .option('-f, --filter ', 'include only package names matching the given string, comma-or-space-delimited list, or /regex/') .option('-g, --global', 'check global packages instead of in the current project') // program.json is set to true in programInit if any options that begin with 'json' are true diff --git a/lib/npm-check-updates.js b/lib/npm-check-updates.js index 57610a25..7619f842 100644 --- a/lib/npm-check-updates.js +++ b/lib/npm-check-updates.js @@ -188,6 +188,10 @@ function analyzeProjectDependencies(options, pkgData, pkgFile) { print(options, `Fetching ${vm.getVersionTarget(options)} versions...`, 'verbose'); + if (options.enginesNode) { + options.enginesNode = _.get(pkg, 'engines.node'); + } + return vm.upgradePackageDefinitions(current, options).then(async ([upgraded, latest]) => { const {newPkgData, selectedNewDependencies} = await vm.upgradePackageData(pkgData, current, upgraded, latest, options); diff --git a/lib/package-managers/npm.js b/lib/package-managers/npm.js index 5bb003ed..e18e3b9f 100644 --- a/lib/package-managers/npm.js +++ b/lib/package-managers/npm.js @@ -6,6 +6,8 @@ const versionUtil = require('../version-util.js'); const spawn = require('spawn-please'); const pacote = require('pacote'); +const TIME_FIELDS = ['modified', 'created']; + // needed until pacote supports full npm config compatibility // See: https://github.com/zkat/pacote/issues/156 const npmConfig = {}; @@ -46,45 +48,81 @@ function parseJson(result, data) { * @param {string} packageName Name of the package * @param {string} field Field such as "versions" or "dist-tags.latest" are parsed from the pacote result (https://www.npmjs.com/package/pacote#packument) * @param {string} currentVersion - * @returns {Promise} Promised result + * @returns {Promise} Promised result */ -function view(packageName, field, currentVersion) { +function viewOne(packageName, field, currentVersion) { + return viewMany(packageName, [field], currentVersion) + .then(result => { + return result && result[field]; + }); +} + +/** + * @param {string} packageName Name of the package + * @param {string[]} fields Array of fields like versions, time, version + * @param {string} currentVersion + * @returns {Promise} Promised result + */ +function viewMany(packageName, fields, currentVersion) { if (currentVersion && (!semver.validRange(currentVersion) || versionUtil.isWildCard(currentVersion))) { return Promise.resolve(); } - npmConfig['full-metadata'] = field === 'time'; + npmConfig['full-metadata'] = _.includes(fields, 'time'); return pacote.packument(packageName, npmConfig).then(result => { - if (field.startsWith('dist-tags.')) { - const [tagName, version] = field.split('.'); - if (result[tagName]) { - return result[tagName][version]; + const resultObject = {}; + _.each(fields, (field) => { + if (field.startsWith('dist-tags.')) { + const [tagName, version] = field.split('.'); + if (result[tagName]) { + resultObject[field] = result.versions[result[tagName][version]]; + } + } else { + resultObject[field] = result[field]; } - } else if (field === 'versions') { - return Object.keys(result[field]); - } else { - return result[field]; - } + }); + return resultObject; }); } /** * @param {Array} versions Array of all available versions + * @param {Boolean} pre Enabled prerelease? * @returns {Array} An array of versions with the release versions filtered out */ -function filterOutPrereleaseVersions(versions) { - return _.filter(versions, _.negate(isPre)); +function filterOutPrereleaseVersions(versions, pre) { + return _.filter(versions, (version) => { + return pre || _.negate(isPre)(version); + }); } /** - * @param version - * @returns {boolean} True if the version is any kind of prerelease: alpha, beta, rc, pre + * @param {String} version + * @returns {boolean} True if the version is any kind of prerelease: alpha, beta, rc, pre */ function isPre(version) { return versionUtil.getPrecision(version) === 'release'; } +/** + * @param {{}} versions Object with all versions + * @param {String} enginesNode Package engines.node range + * @returns {Array} An array of versions which satisfies engines.node range + */ +function doesSatisfyEnginesNode(versions, enginesNode) { + if (!enginesNode) { + return _.keys(versions); + } + const minVersion = _.get(semver.minVersion(enginesNode), 'version'); + if (!minVersion) { + return _.keys(versions); + } + return _.keys(versions).filter((version) => { + let versionEnginesNode = _.get(versions[version], 'engines.node'); + return versionEnginesNode && semver.satisfies(minVersion, versionEnginesNode); + }); +} /** * Spawn npm requires a different command on Windows. @@ -159,22 +197,28 @@ module.exports = { /** * @param {string} packageName * @param {string} currentVersion - * @param {boolean} pre + * @param {{}} options * @returns {Promise} */ - latest(packageName, currentVersion, pre) { - return view(packageName, 'dist-tags.latest', currentVersion) - .then(version => { - // if latest is not a prerelease version, return it - // if latest is a prerelease version and --pre is specified, return it - if (!isPre(version) || pre) { - return version; + latest(packageName, currentVersion, options) { + return viewOne(packageName, 'dist-tags.latest', currentVersion) + .then(latest => { + // if latest exists and latest not satisfies min version of engines.node, set null to it + if (latest && !doesSatisfyEnginesNode({[latest.version]: latest}, options.enginesNode).length) { + latest = null; + } + // if latest exists and latest is not a prerelease version, return it + // if latest exists and latest is a prerelease version and --pre is specified, return it + if (latest && (!isPre(latest.version) || options.pre)) { + return latest.version; // if latest is a prerelease version and --pre is not specified, find the next // version that is not a prerelease } else { - return view(packageName, 'versions', currentVersion) - .then(filterOutPrereleaseVersions) - .then(_.last); + return viewOne(packageName, 'versions', currentVersion) + .then(versions => { + versions = doesSatisfyEnginesNode(versions, options.enginesNode); + return _.last(filterOutPrereleaseVersions(versions, options.pre)); + }); } }); }, @@ -182,55 +226,68 @@ module.exports = { /** * @param {string} packageName * @param {string} currentVersion - * @param {boolean} pre + * @param {{}} options * @returns {Promise} */ - newest(packageName, currentVersion, pre) { - return view(packageName, 'time', currentVersion) - .then(_.keys) - .then(_.partialRight(_.pullAll, ['modified', 'created'])) + newest(packageName, currentVersion, options) { + return viewMany(packageName, ['time', 'versions'], currentVersion) + .then((result) => { + const versions = doesSatisfyEnginesNode(result.versions, options.enginesNode); + _.keys(result.time).forEach((key) => { + if (!_.includes(TIME_FIELDS, key) && !_.includes(versions, key)) { + delete result.time[key]; + } + }); + return _.keys(result.time); + }) + .then(_.partialRight(_.pullAll, TIME_FIELDS)) .then(versions => { - return _.last(pre ? versions : filterOutPrereleaseVersions(versions)); + return _.last(filterOutPrereleaseVersions(versions, options.pre)); }); }, /** * @param {string} packageName * @param {string} currentVersion - * @param {boolean} pre + * @param {{}} options * @returns {Promise} */ - greatest(packageName, currentVersion, pre) { - return view(packageName, 'versions', currentVersion) + greatest(packageName, currentVersion, options) { + return viewOne(packageName, 'versions', currentVersion) .then(versions => { - return _.last(pre ? versions : filterOutPrereleaseVersions(versions)); + versions = doesSatisfyEnginesNode(versions, options.enginesNode); + return _.last(filterOutPrereleaseVersions(versions, options.pre)); }); }, /** * @param {string} packageName * @param {string} currentVersion - * @param {boolean} pre + * @param {{}} options * @returns {Promise} */ - greatestMajor(packageName, currentVersion, pre) { - return view(packageName, 'versions', currentVersion).then(versions => { - const resultVersions = pre ? versions : filterOutPrereleaseVersions(versions); - return versionUtil.findGreatestByLevel(resultVersions, currentVersion, 'major'); - }); + greatestMajor(packageName, currentVersion, options) { + return viewOne(packageName, 'versions', currentVersion) + .then(versions => { + versions = doesSatisfyEnginesNode(versions, options.enginesNode); + versions = filterOutPrereleaseVersions(versions, options.pre); + return versionUtil.findGreatestByLevel(versions, currentVersion, 'major'); + }); }, /** * @param {string} packageName * @param {string} currentVersion - * @param {boolean} pre + * @param {{}} options * @returns {Promise} */ - greatestMinor(packageName, currentVersion, pre) { - return view(packageName, 'versions', currentVersion).then(versions => { - const resultVersions = pre ? versions : filterOutPrereleaseVersions(versions); - return versionUtil.findGreatestByLevel(resultVersions, currentVersion, 'minor'); - }); + greatestMinor(packageName, currentVersion, options) { + return viewOne(packageName, 'versions', currentVersion) + .then(versions => { + versions = doesSatisfyEnginesNode(versions, options.enginesNode); + versions = filterOutPrereleaseVersions(versions, options.pre); + return versionUtil.findGreatestByLevel(versions, currentVersion, 'minor'); + }); }, defaultPrefix diff --git a/lib/versionmanager.js b/lib/versionmanager.js index 48c716d5..eeb9f67d 100644 --- a/lib/versionmanager.js +++ b/lib/versionmanager.js @@ -253,7 +253,8 @@ function upgradePackageDefinitions(currentDependencies, options) { pre: options.pre, packageManager: options.packageManager, json: options.json, - loglevel: options.loglevel + loglevel: options.loglevel, + enginesNode: options.enginesNode }).then(latestVersions => { const upgradedDependencies = upgradeDependencies(currentDependencies, latestVersions, { @@ -422,7 +423,7 @@ function queryVersions(packageMap, options = {}) { * @returns {Promise} */ function getPackageVersionProtected(dep) { - return getPackageVersion(dep, packageMap[dep], options.pre).catch(err => { + return getPackageVersion(dep, packageMap[dep], options).catch(err => { if (err && (err.message || err).toString().match(/E404|ENOTFOUND|404 Not Found/i)) { return null; } else { diff --git a/test/test-ncu.js b/test/test-ncu.js index dd862526..2ff56456 100644 --- a/test/test-ncu.js +++ b/test/test-ncu.js @@ -46,7 +46,7 @@ describe('npm-check-updates', function () { ]); }); - it('should suggest upgrades to versions within the specified version range if jsonUpraded is true', () => { + it('should suggest upgrades to versions within the specified version range if jsonUpgraded is true', () => { const upgraded = ncu.run({ // juggernaut has been deprecated at v2.1.1 so it is unlikely to invalidate this test packageData: '{ "dependencies": { "juggernaut": "^2.1.0" } }', @@ -61,7 +61,7 @@ describe('npm-check-updates', function () { ]); }); - it('should not suggest upgrades to versions within the specified version range if jsonUpraded is true and minimial is true', () => { + it('should not suggest upgrades to versions within the specified version range if jsonUpgraded is true and minimial is true', () => { const upgraded = ncu.run({ // juggernaut has been deprecated at v2.1.1 so it is unlikely to invalidate this test packageData: '{ "dependencies": { "juggernaut": "^2.1.0" } }', @@ -226,6 +226,85 @@ describe('npm-check-updates', function () { }); }); + it('should enable --engines-node matching ', () => { + return ncu.run({ + jsonAll: true, + packageData: JSON.stringify({ + dependencies: { + 'del': '3.0.0' + }, + engines: { + 'node': '>=6' + } + }), + enginesNode: true + }).then(data => { + return data.should.eql({ + dependencies: { + 'del': '4.1.1' + }, + engines: { + 'node': '>=6' + } + }); + }); + }); + + it('should enable engines matching if --engines-node', () => { + return ncu.run({ + jsonAll: true, + packageData: JSON.stringify({ + dependencies: { + 'del': '3.0.0' + }, + engines: { + 'node': '>=6' + } + }), + enginesNode: true + }).then(upgradedPkg => { + upgradedPkg.should.have.property('dependencies'); + upgradedPkg.dependencies.should.have.property('del'); + upgradedPkg.dependencies.del.should.equal('4.1.1'); + }); + }); + + it('should enable engines matching if --engines-node, not update if matches not exists', () => { + return ncu.run({ + jsonAll: true, + packageData: JSON.stringify({ + dependencies: { + 'del': '3.0.0' + }, + engines: { + 'node': '>=1' + } + }), + enginesNode: true + }).then(upgradedPkg => { + upgradedPkg.should.have.property('dependencies'); + upgradedPkg.dependencies.should.have.property('del'); + upgradedPkg.dependencies.del.should.equal('3.0.0'); + }); + }); + + it('should enable engines matching if --engines-node, update to latest version if engines.node not exists', () => { + return ncu.run({ + jsonAll: true, + packageData: JSON.stringify({ + dependencies: { + 'del': '3.0.0' + } + }), + enginesNode: true + }).then(upgradedPkg => { + upgradedPkg.should.have.property('dependencies'); + upgradedPkg.dependencies.should.have.property('del'); + upgradedPkg.dependencies.del.should.not.equal('3.0.0'); + upgradedPkg.dependencies.del.should.not.equal('4.1.1'); + }); + }); + }); describe('cli', () => {