@springernature/util-package-finder
Version:
Get a list of packages and available versions from within an NPM scope
223 lines (203 loc) • 5.26 kB
JavaScript
/**
* Package Finder
* Search for packages within the NPM registry
*/
;
const fetch = require('node-fetch');
const _ = require('lodash/fp');
const orderBy = require('lodash/orderBy');
const semver = require('semver');
/**
* Filter package results by name
* @private
* @param {Object} json search results
* @param {Object} opts search options
* @return {Promise<Object>}
*/
const filterResults = (json, opts) => {
return new Promise(resolve => {
// API returns results in an "objects" key in the top level
// "results" makes for clearer code
json.results = json.objects;
delete json.objects;
// first filter by scope
json.results = json.results.filter(
item => {
const regex = new RegExp(`@${opts.scope}/`);
return regex.test(item.package.name);
}
);
// filter by user filters
if (opts.filters.length === 0) {
resolve(json);
return;
}
const packageNameRex = new RegExp(`/(${opts.filters.join('|')})`, 'i');
json.results = json.results.filter(item => packageNameRex.test(item.package.name));
resolve(json);
});
};
/**
* Check if version number is valid semver
* @private
* @param {String} version single version number
* @return {Boolean}
*/
const isValidSemver = version => {
return semver.valid(version);
};
/**
* Sorts an array of versions in descending order
* @private
* @param {Object} versions all versions as json
* @return {Array}
*/
const sortVersions = versions => {
return Object.keys(versions)
.filter(isValidSemver)
.sort(semver.rcompare);
};
/**
* Get all versions of each package from registry
* @private
* @param {Object} json search results
* @param {Boolean} versions show versions
* @return {Promise<Object>}
*/
const getVersions = (json, versions) => {
return new Promise((resolve, reject) => {
const promises = [];
if (!versions) {
resolve(json);
return;
}
json.results
.map(n => n.package.name)
.forEach(name => {
const promise = fetch(`http://registry.npmjs.com/${encodeURIComponent(name)}`)
.then(response => response.json())
.then(packageJson => {
json.results
.filter(item => {
if (item.package.name === packageJson._id) {
item.package.versions = sortVersions(packageJson.versions);
}
return true;
});
})
.catch(err => reject(err));
promises.push(promise);
});
Promise.all(promises).then(() => {
resolve(json);
});
});
};
/**
* Get status of package
* @private
* @param {Object} json package details
* @return {String}
*/
const getStatus = json => {
if (semver.gte(json.package.version, '1.0.0')) {
return 'production';
}
if (semver.gte(json.package.version, '0.1.0')) {
return 'development';
}
if (semver.gte(json.package.version, '0.0.1')) {
return 'experimental';
}
};
/**
* Set status of package based on latest version
* @private
* @param {Object} json search results
* @return {Promise<Array>}
*/
const setStatus = json => {
return new Promise(resolve => {
json.results
.filter(item => {
item.package.status = getStatus(item);
return true;
});
resolve(json);
});
};
/**
* Get the interpolated search URI
* @private
* @param {String} subject freetext string to search for
* @return {String}
*/
const getPackagesSearchURI = subject => {
const limit = 250;
return `https://registry.npmjs.com/-/v1/search?text=${subject}&size=${limit}`;
};
/**
* Validate default function options
* @private
* @param {Object} opts search options
* @return {Promise<Object>}
*/
const validateOptions = opts => {
return new Promise((resolve, reject) => {
if (!Array.isArray(opts.filters)) {
reject(new Error(`Filters parameter must be of type \`array\`, found \`${typeof opts.filters}\``));
}
if (typeof opts.versions !== 'boolean') {
reject(new Error(`Versions parameter must be of type \`boolean\`, found \`${typeof opts.versions}\``));
}
resolve(opts);
});
};
/**
* Removes any scope prefix
* @private
* @param {String} scope search scope
* @param {String} scope search scope with any scope prefix removed
*/
const normaliseScopePrefix = scope => {
if (scope.startsWith('@')) {
scope = scope.substr(1);
}
return scope;
};
/**
* Get all non-deprecated packages for a scope, with additional filtering
* @param {Object} defaults
* @return {Promise<Array>}
*/
module.exports = ({
scope = 'springernature',
filters = [],
versions = false
} = {}) => (
validateOptions({
scope: normaliseScopePrefix(scope),
filters: filters,
versions: versions
})
.then(opts => fetch(getPackagesSearchURI(opts.scope)))
.then(response => response.json())
.then(json => filterResults(json, {scope: normaliseScopePrefix(scope), filters: filters}))
.then(json => getVersions(json, versions))
.then(json => setStatus(json))
.then(json => _.flow(
_.map(item => ({
name: item.package.name,
latest: item.package.version,
versions: (versions) ? item.package.versions : null,
status: item.package.status,
description: item.package.description,
npm: item.package.links.npm,
date: item.package.date
}))
)(json.results))
.then(arr => orderBy(arr, item => item.name, ['asc']))
.catch(err => {
throw err;
})
);