UNPKG

next-update

Version:

Tests if module's dependencies can be updated to the newer version without breaking the tests

336 lines (283 loc) 9.64 kB
'use strict' var la = require('lazy-ass') var check = require('check-more-types') var log = require('debug')('next-update') const R = require('ramda') var request = require('request') var verify = check.verify var semver = require('semver') var q = require('q') var localVersion = require('./local-module-version') var isUrl = require('npm-utils').isUrl var _ = require('lodash') const {isPrerelease} = require('./utils') var _registryUrl = require('npm-utils').registryUrl la(check.fn(_registryUrl), 'expected registry url function') var registryUrl = _.once(_registryUrl) const notPrerelease = R.complement(isPrerelease) function cleanVersion (version, name) { var originalVersion = version verify.unemptyString(version, 'missing version string' + version) verify.unemptyString(name, 'missing name string' + name) if (isUrl(version)) { // version = version.substr(version.indexOf('#') + 1); // hmm, because we don't have a way to fetch git tags yet // just skip these dependencies console.log('Cannot handle git repos, skipping', name, 'at', version) return } if (version === 'original' || version === 'modified' || version === 'created') { return } version = version.replace('~', '').replace('^', '') var twoDigitVersion = /^\d+\.\d+$/ if (twoDigitVersion.test(version)) { version += '.0' } if (version === 'latest' || version === '*') { console.log('Module', name, 'uses version', version) console.log('It is recommented to list a specific version number') version = localVersion(name) if (!version) { version = '0.0.1' } console.log('module', name, 'local version', version) } try { version = semver.clean(version) } catch (err) { console.error('exception when cleaning version', version) return } if (!version) { log('could not clean version ' + originalVersion + ' for ' + name) return } console.assert(version, 'missing clean version ' + originalVersion + ' for ' + name) return version } // eslint-disable-next-line no-unused-vars function cleanVersionPair (nameVersion) { check.verify.array(nameVersion, 'expected an array') console.assert(nameVersion.length === 2, 'expected 2 items, name and version ' + nameVersion) var name = nameVersion[0] check.verify.string(name, 'could not get module name from ' + nameVersion) var version = nameVersion[1] check.verify.string(version, 'could not get module version from ' + nameVersion) version = cleanVersion(version, name) if (!version) { return } nameVersion[1] = version return nameVersion } function cleanVersionObject (info) { check.verify.object(info, 'expected info') var name = info.name check.verify.string(name, 'could not get module name from ' + info) var version = info.version check.verify.string(version, 'could not get module version from ' + info) version = cleanVersion(version, name) if (!version) { return } info.version = version return info } function cleanVersions (nameVersionPairs) { check.verify.array(nameVersionPairs, 'expected array') var cleaned = nameVersionPairs .map(cleanVersionObject) .filter(check.object) return cleaned } function formNpmErrorMessage (name, info) { var reason = info.reason || info.error || JSON.stringify(info) var str = 'ERROR in npm info for ' + name + ' reason ' + reason return str } function cleanVersionFor (name, version) { return cleanVersion(version, name) } function extractVersions (info) { if (info.time) { return Object.keys(info.time) } if (info.versions) { return Object.keys(info.versions) } } function is404 (response) { return response && response.statusCode === 404 } function isNotFound (str) { var moduleNotFound = (/not found/).test(str) var cannotConnect = (/ENOTFOUND/).test(str) var errorInNpm = (/ERROR in npm/).test(str) var couldNotFetch = (/could not fetch/i).test(str) return moduleNotFound || cannotConnect || errorInNpm || couldNotFetch } // fetching versions inspired by // https://github.com/jprichardson/npm-latest // returns a promise function fetchVersions (options, nameVersion) { // console.log(nameVersion); // TODO use check.schema check.verify.object(nameVersion, 'expected name, version object') var name = nameVersion.name var version = nameVersion.version check.verify.string(name, 'missing name string') check.verify.string(version, 'missing version string') var cleanVersionForName = _.partial(cleanVersionFor, name) function isLaterVersion (ver) { var later = semver.gt(ver, version) return later } // console.log('fetching versions for', name, 'current version', version); var MAX_WAIT_TIMEOUT = options.checkVersionTimeout || 25000 var deferred = q.defer() function rejectOnTimeout () { var msg = 'timed out waiting for NPM for package ' + name + ' after ' + MAX_WAIT_TIMEOUT + 'ms' console.error(msg) deferred.reject(msg) } function escapeName (str) { return str.replace('/', '%2F') } registryUrl().then(function (npmUrl) { log('NPM registry url', npmUrl) la(check.webUrl(npmUrl), 'need npm registry url, got', npmUrl) npmUrl = npmUrl.replace(/^https:/, 'http:').trim() var url = (options.registry || npmUrl) + escapeName(name) // TODO how to detect if the registry is not responding? log('getting url', url) request.get(url, onNPMversions) var timer = setTimeout(rejectOnTimeout, MAX_WAIT_TIMEOUT) function onNPMversions (err, response, body) { log('got response for', url) clearTimeout(timer) if (err) { console.error('ERROR when fetching info for package', name) deferred.reject(err.message) return } if (is404(response)) { log('404 response for', url) deferred.resolve({ name: name, versions: [] }) return } try { log('parsing response body') var info = JSON.parse(body) log('parsed response, error?', info.error) if (info.error) { log('error parsing\n' + body + '\n') var str = formNpmErrorMessage(name, info) console.error(str) if (isNotFound(info.error)) { deferred.resolve({ name: name, versions: [] }) return } deferred.reject(str) return } log('extracting versions') var versions = extractVersions(info) log('%d versions', versions.length) if (!Array.isArray(versions)) { throw new Error('Could not get versions for ' + name + ' from ' + JSON.stringify(info) + ' response ' + JSON.stringify(response, null, 2)) } var validVersions = versions.filter(cleanVersionForName) var newerVersions = validVersions.filter(isLaterVersion) log('%d valid versions', validVersions.length) if (validVersions.length) { log('last version %s', R.last(validVersions)) } if (newerVersions.length) { log('%d newer versions', newerVersions.length) log('from %s to %s', R.head(newerVersions), R.last(newerVersions)) } deferred.resolve({ name: name, versions: newerVersions }) return } catch (err) { console.error(err) deferred.reject('Could not fetch versions for ' + name) } } }) return deferred.promise } const verboseLog = (options) => options.tldr ? _.noop : console.log function logFetched (fetched) { fetched.forEach(individual => { log('%s - %d versions', individual.name, individual.versions.length) }) } const hasVersions = nameNewVersions => nameNewVersions && check.array(nameNewVersions.versions) && check.unempty(nameNewVersions.versions) const filterVersions = R.evolve({ versions: R.filter(notPrerelease) }) function filterFetchedVersions (checkLatestOnly, results) { la(arguments.length === 2, 'expected two arguments', arguments) checkLatestOnly = Boolean(checkLatestOnly) check.verify.array(results, 'expected list of results') log('fetch all new version results') logFetched(results) let available = results .map(filterVersions) .filter(hasVersions) if (checkLatestOnly) { available = available.map(function (nameVersions) { if (nameVersions.versions.length > 1) { nameVersions.versions = nameVersions.versions.slice(-1) } return nameVersions }) } else { verboseLog('checking ALL versions') } return available } // returns a promise with available new versions function nextVersions (options, nameVersionPairs, checkLatestOnly) { check.verify.object(options, 'expected object with options') check.verify.array(nameVersionPairs, 'expected array') nameVersionPairs = cleanVersions(nameVersionPairs) const verbose = verboseLog(options) verbose('checking NPM registry') var MAX_CHECK_TIMEOUT = options.checkVersionTimeout || 10000 var fetchPromises = nameVersionPairs.map(fetchVersions.bind(null, options)) var fetchAllPromise = q.all(fetchPromises) .timeout(MAX_CHECK_TIMEOUT, 'timed out waiting for NPM after ' + MAX_CHECK_TIMEOUT + 'ms') return fetchAllPromise.then( _.partial(filterFetchedVersions, checkLatestOnly), q.reject ) } module.exports = { isUrl, cleanVersion, cleanVersions, fetchVersions, nextVersions, filterFetchedVersions, hasVersions }