Reputation: 25383
I'm writing a node script which helps pin dependencies.
How can I determine the greatest realized version of a package existing on an NPM server, from a semver version?
For example, we have a dependency "foo" which is specified in a package.json as ~1.2.3
.
Out on NPM, there exists published version 1.2.5
, which is the latest published version compatible with ~1.2.3
.
I need to write a script that would take as input "foo" and ~1.2.3
, then after a server query, return 1.2.5
. Something like this:
await fetchRealizedVersion('foo', '~1.2.3'); // resolves to 1.2.5
I understand I could do something like yarn upgrade
and then parse the lock file, but I am looking for a more direct way of accomplishing this.
Hopefully there is a package that boils this down to an API call, but I'm not finding anything after googling around.
Upvotes: 6
Views: 1371
Reputation: 24992
"Hopefully there is a package that boils this down to an API call,"
Short Answer: Unfortunately no, there is not a package that currently exists as far as I know.
Edit: There is the get-latest-version
package you may want to try:
Basic usage:
const getLatestVersion = require('get-latest-version') getLatestVersion('some-other-module', {range: '^1.0.0'}) .then((version) => console.log(version)) // highest version matching ^1.0.0 range .catch((err) => console.error(err))
Alternatively, consider utilizing/writing a custom node.js module to perform the following steps:
Either:
Shell out the npm view command to retrieve all versions that are available in the NPM registry for a given package: For instance:
npm view <pkg> versions --json
Or, directly make a https
request to the public npm registry at https://registry.npmjs.org
to retrieve all versions available in for a given package.
Parse the JSON returned and pass it, along with the semver range (e.g. ~1.2.3
), to the node-semver package's maxSatisfying()
method.
The maxSatisfying()
method is described in the docs as:
maxSatisfying(versions, range)
: Return the highest version in the list that satisfies the range, ornull
if none of them do.
The custom example module provided in get-latest-version.js
(below) essentially performs the aforementioned steps. In this example we shell out the npm view
command.
get-latest-version.js
'use strict';
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const { exec } = require('child_process');
const { maxSatisfying } = require('semver');
//------------------------------------------------------------------------------
// Data
//------------------------------------------------------------------------------
const errorBadge = '\x1b[31;40mERR!\x1b[0m';
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Captures the data written to stdout from a given shell command.
*
* @param {String} command The shell command to execute.
* @return {Promise<string>} A Promise object whose fulfillment value holds the
* data written to stdout. When rejected an error message is returned.
* @private
*/
function shellExec(command) {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(new Error(`Failed executing command: '${command}'`));
return;
}
resolve(stdout.trim());
});
});
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
/**
* Retrieves the latest version that matches the given range for a package.
*
* @async
* @param {String} pkg The package name.
* @param {String} range The semver range.
* @returns {Promise<string>} A Promise object that when fulfilled returns the
* latest version that matches. When rejected an error message is returned.
*/
async fetchRealizedVersion(pkg, range) {
try {
const response = await shellExec(`npm view ${pkg} versions --json`);
const versions = JSON.parse(response);
return maxSatisfying(versions, range);
} catch ({ message: errorMssg }) {
throw Error([
`${errorBadge} ${errorMssg}`,
`${errorBadge} '${pkg}' is probably not in the npm registry.`
].join('\n'));
}
}
};
The following index.js
demonstrates using the aforementioned module.
index.js
'use strict';
const { fetchRealizedVersion } = require('./get-latest-version.js');
(async function() {
try {
const latest = await fetchRealizedVersion('eslint', '~5.15.0');
console.log(latest); // --> 5.15.3
} catch ({ message: errMssg }) {
console.error(errMssg);
}
})();
As you can see, in that example we obtain the latest published version for the eslint package that is compatible with the semver tilde range ~5.15.0
.
The latest/maximum version that satisfies ~5.15.0
is printed to the console:
$ node ./index.js 5.15.3
Note: You can always double check the results using the online semver calculator which actually utilizes the node-semver
package.
The following index.js
demonstrates using the aforementioned module to obtain the latest/maximum version for multiple packages and different ranges.
index.js
'use strict';
const { fetchRealizedVersion } = require('./get-latest-version.js');
const criteria = [
{
pkg: 'eslint',
range: '^4.9.0'
},
{
pkg: 'eslint',
range: '~5.0.0'
},
{
pkg: 'lighthouse',
range: '~1.0.0'
},
{
pkg: 'lighthouse',
range: '^1.0.4'
},
{
pkg: 'yarn',
range: '~1.3.0'
},
{
pkg: 'yarn',
range: '^1.3.0'
},
{
pkg: 'yarn',
range: '^20.3.0'
},
{
pkg: 'quuxbarfoo',
range: '~1.3.0'
}
];
(async function () {
// Each request is sent and read in parallel.
const promises = criteria.map(async ({ pkg, range }) => {
try {
return await fetchRealizedVersion(pkg, range);
} catch ({ message: errMssg }) {
return errMssg;
}
});
// Log each 'latest' semver in sequence.
for (const latest of promises) {
console.log(await latest);
}
})();
The result for that last example is as follows:
$ node ./index.js 4.19.1 5.0.1 1.0.6 1.6.5 1.3.2 1.22.4 null ERR! Failed executing command: 'npm view quuxbarfoo versions --json' ERR! 'quuxbarfoo' is probably not in the npm registry.
Additional Note: The shellExec
helper function in get-latest-version.js
currently promisifies the child_process
module's exec()
method to shell out the npm view
command. However, since node.js version 12 the built-in util.promisify
provides another way to promisify the exec()
method (as shown in the docs for exec
), so you may prefer to do it that way instead.
If you wanted to avoid shelling out the npm view
command you could consider making a request directly to the https://registry.npmjs.org
endpoint instead (which is the same endpoint that the npm view
command sends a https GET
request to).
The modified version of get-latest-version.js
(below) essentially utilizes a promisified version of the builtin https.get
.
Usage is the same as demonstrated previously in the "Usage" section.
get-latest-version.js
'use strict';
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const https = require('https');
const { maxSatisfying } = require('semver');
//------------------------------------------------------------------------------
// Data
//------------------------------------------------------------------------------
const endPoint = 'https://registry.npmjs.org';
const errorBadge = '\x1b[31;40mERR!\x1b[0m';
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Requests JSON for a given package from the npm registry.
*
* @param {String} pkg The package name.
* @return {Promise<json>} A Promise object that when fulfilled returns the JSON
* metadata for the specific package. When rejected an error message is returned.
* @private
*/
function fetchPackageInfo(pkg) {
return new Promise((resolve, reject) => {
https.get(`${endPoint}/${pkg}/`, response => {
const { statusCode, headers: { 'content-type': contentType } } = response;
if (statusCode !== 200) {
reject(new Error(`Request to ${endPoint} failed. ${statusCode}`));
return;
}
if (!/^application\/json/.test(contentType)) {
reject(new Error(`Expected application/json but received ${contentType}`));
return;
}
let data = '';
response.on('data', chunk => {
data += chunk;
});
response.on('end', () => {
resolve(data);
});
}).on('error', error => {
reject(new Error(`Cannot find ${endPoint}`));
});
});
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
module.exports = {
/**
* Retrieves the latest version that matches the given range for a package.
*
* @async
* @param {String} pkg The package name.
* @param {String} range The semver range.
* @returns {Promise<string>} A Promise object that when fulfilled returns the
* latest version that matches. When rejected an error message is returned.
*/
async fetchRealizedVersion(pkg, range) {
try {
const response = await fetchPackageInfo(pkg);
const { versions: allVersionInfo } = JSON.parse(response);
// The response includes all metadata for all versions of a package.
// Let's create an Array holding just the `version` info.
const versions = [];
Object.keys(allVersionInfo).forEach(key => {
versions.push(allVersionInfo[key].version)
});
return maxSatisfying(versions, range);
} catch ({ message: errorMssg }) {
throw Error([
`${errorBadge} ${errorMssg}`,
`${errorBadge} '${pkg}' is probably not in the npm registry.`
].join('\n'));
}
}
};
Note The version of node-semver used in the example custom modules (A & B) IS NOT the current latest version (i.e. 7.3.2
). Version ^5.7.1
was used instead - which is the same version used by the npm cli tool.
Upvotes: 4