Skip to content

Commit

Permalink
Added --engines-node functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
vanodevium committed Nov 2, 2019
1 parent 50b444e commit a67d8a3
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 54 deletions.
1 change: 1 addition & 0 deletions bin/ncu
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ program
.option('--cwd <path>', 'Used as current working directory for `spawn` in npm listing')
.option('--dep <dep>', 'check only a specific section(s) of dependencies: prod|dev|peer|optional|bundle (comma-delimited)')
.option('-e, --error-level <n>', '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 <matches>', '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
Expand Down
2 changes: 1 addition & 1 deletion bin/npm-check-updates
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env node
'use strict';

require('node-alias')('ncu', __dirname, { message: false });
require('node-alias')('ncu', __dirname, {message: false});
4 changes: 4 additions & 0 deletions lib/npm-check-updates.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
155 changes: 106 additions & 49 deletions lib/package-managers/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -159,78 +197,97 @@ 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));
});
}
});
},

/**
* @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
Expand Down
5 changes: 3 additions & 2 deletions lib/versionmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -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 {
Expand Down
83 changes: 81 additions & 2 deletions test/test-ncu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" } }',
Expand All @@ -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" } }',
Expand Down Expand Up @@ -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', () => {
Expand Down

0 comments on commit a67d8a3

Please sign in to comment.