diff --git a/lib/manager/docker/package.js b/lib/manager/docker/package.js
index 67d5f1f9022f5933e1402f936b806b2e1e1f283f..035ae0b885e0d1788a8b11fda9e4d4ac7202433b 100644
--- a/lib/manager/docker/package.js
+++ b/lib/manager/docker/package.js
@@ -1,4 +1,4 @@
-const { getMajor, isValidSemver } = require('../../versioning/semver');
+const { getMajor, isValid } = require('../../versioning/semver');
 const dockerApi = require('../../datasource/docker');
 const compareVersions = require('compare-versions');
 
@@ -57,7 +57,7 @@ async function getPackageUpdates(config) {
   if (currentTag) {
     const tagVersion = getVersion(currentTag);
     const tagSuffix = getSuffix(currentTag);
-    if (!isValidSemver(tagVersion)) {
+    if (!isValid(tagVersion)) {
       logger.info(
         { currentDepTag },
         'Docker tag is not valid semver - skipping'
@@ -72,7 +72,7 @@ async function getPackageUpdates(config) {
       versionList = allTags
         .filter(tag => getSuffix(tag) === tagSuffix)
         .map(getVersion)
-        .filter(isValidSemver)
+        .filter(isValid)
         .filter(
           version =>
             // All stable are allowed
diff --git a/lib/manager/npm/lookup/filter.js b/lib/manager/npm/lookup/filter.js
new file mode 100644
index 0000000000000000000000000000000000000000..2d3b17b10ac7a63aa03fe71d318506ecb92e2650
--- /dev/null
+++ b/lib/manager/npm/lookup/filter.js
@@ -0,0 +1,63 @@
+const {
+  getMajor,
+  isGreaterThan,
+  isStable,
+} = require('../../../versioning/semver');
+
+module.exports = {
+  filterVersions,
+  filterUnstable,
+  filterLatest,
+};
+
+function filterVersions(config, fromVersion, latestVersion, versions) {
+  const { ignoreUnstable, respectLatest } = config;
+  // Leave only versions greater than current
+  let filteredVersions = versions.filter(version =>
+    isGreaterThan(version, fromVersion)
+  );
+  filteredVersions = filterUnstable(
+    ignoreUnstable,
+    fromVersion,
+    filteredVersions
+  );
+  filteredVersions = filterLatest(
+    respectLatest,
+    fromVersion,
+    latestVersion,
+    filteredVersions
+  );
+  return filteredVersions;
+}
+
+function filterUnstable(ignoreUnstable, fromVersion, versions) {
+  // Filter nothing out if we are not ignoring unstable
+  if (ignoreUnstable === false) {
+    return versions;
+  }
+  // Filter out all unstable if fromVersion is stable
+  if (isStable(fromVersion)) {
+    // Remove all unstable
+    return versions.filter(isStable);
+  }
+  // Allow unstable only in current major
+  return versions.filter(
+    version => isStable(version) || getMajor(version) === getMajor(fromVersion)
+  );
+}
+
+function filterLatest(respectLatest, fromVersion, latestVersion, versions) {
+  // No filtering if no latest
+  if (!latestVersion) {
+    return versions;
+  }
+  // No filtering if not respecting latest
+  if (respectLatest === false) {
+    return versions;
+  }
+  // No filtering if fromVersion is already past latest
+  if (isGreaterThan(fromVersion, latestVersion)) {
+    return versions;
+  }
+  return versions.filter(version => !isGreaterThan(version, latestVersion));
+}
diff --git a/lib/manager/npm/lookup/index.js b/lib/manager/npm/lookup/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..2e25482e8baa4ae5860b61ee18562e48ffd427ff
--- /dev/null
+++ b/lib/manager/npm/lookup/index.js
@@ -0,0 +1,151 @@
+const versioning = require('../../../versioning/semver');
+const moment = require('moment');
+const { getRollbackUpdate } = require('./rollback');
+
+const { filterVersions } = require('./filter');
+
+const {
+  getMajor,
+  getMinor,
+  isGreaterThan,
+  isRange,
+  matches,
+  maxSatisfyingVersion,
+  minSatisfyingVersion,
+  rangify,
+} = versioning;
+
+module.exports = {
+  lookupUpdates,
+};
+
+function lookupUpdates(dependency, config) {
+  const { currentVersion, lockedVersion, rangeStrategy } = config;
+  const { latestVersion } = dependency;
+  const allVersions = Object.keys(dependency.versions);
+  if (allVersions.length === 0) {
+    const message = `No versions returned from registry for this package`;
+    logger.warn({ dependency }, message);
+    return [
+      {
+        type: 'warning',
+        message,
+      },
+    ];
+  }
+  const allSatisfyingVersions = allVersions.filter(version =>
+    matches(version, currentVersion)
+  );
+  if (!allSatisfyingVersions.length) {
+    return getRollbackUpdate(config, allVersions);
+  }
+  const fromVersion = getFromVersion(
+    currentVersion,
+    rangeStrategy,
+    lockedVersion,
+    allVersions
+  );
+  const updates = [];
+  if (isRange(currentVersion) && config.rangeStrategy === 'pin') {
+    updates.push({
+      type: 'pin',
+      isPin: true,
+      newVersion: fromVersion,
+      newVersionMajor: getMajor(fromVersion),
+      unpublishable: false,
+    });
+  }
+  const filteredVersions = filterVersions(
+    config,
+    fromVersion,
+    latestVersion,
+    allVersions
+  );
+  if (!filteredVersions.length) {
+    return updates;
+  }
+  const buckets = {};
+  for (const toVersion of filteredVersions) {
+    const update = { fromVersion, toVersion };
+    update.newVersion = rangify(config, currentVersion, fromVersion, toVersion);
+    if (!update.newVersion || update.newVersion === currentVersion) {
+      continue; // eslint-disable-line no-continue
+    }
+    update.newVersionMajor = getMajor(toVersion);
+    update.newVersionMinor = getMinor(toVersion);
+    update.type = getType(config, fromVersion, toVersion);
+    if (isRange(update.newVersion)) {
+      update.isRange = true;
+    }
+
+    // TODO: move unpublishable to npm-specific
+    const version = dependency.versions[toVersion];
+    const elapsed = version ? moment().diff(moment(version.time), 'days') : 999;
+    update.unpublishable = elapsed > 0;
+    // end TODO
+
+    const bucket = getBucket(config, update);
+    if (buckets[bucket]) {
+      if (isGreaterThan(update.toVersion, buckets[bucket].toVersion)) {
+        buckets[bucket] = update;
+      }
+    } else {
+      buckets[bucket] = update;
+    }
+  }
+  return updates.concat(Object.values(buckets));
+}
+
+function getType(config, fromVersion, toVersion) {
+  if (getMajor(toVersion) > getMajor(fromVersion)) {
+    return 'major';
+  }
+  if (getMinor(toVersion) > getMinor(fromVersion)) {
+    return 'minor';
+  }
+  if (config.separateMinorPatch) {
+    return 'patch';
+  }
+  if (config.patch.automerge && !config.minor.automerge) {
+    return 'patch';
+  }
+  return 'minor';
+}
+
+function getBucket(config, update) {
+  const { separateMajorMinor, separateMultipleMajor } = config;
+  const { type, newVersionMajor } = update;
+  if (
+    !separateMajorMinor ||
+    config.groupName ||
+    config.major.automerge === true ||
+    (config.automerge && config.major.automerge !== false)
+  ) {
+    return 'latest';
+  }
+  if (separateMultipleMajor && type === 'major') {
+    return `major-${newVersionMajor}`;
+  }
+  return type;
+}
+
+function getFromVersion(
+  currentVersion,
+  rangeStrategy,
+  lockedVersion,
+  allVersions
+) {
+  if (!isRange(currentVersion)) {
+    return currentVersion;
+  }
+  logger.trace(`currentVersion ${currentVersion} is range`);
+  if (rangeStrategy === 'pin') {
+    return lockedVersion || maxSatisfyingVersion(allVersions, currentVersion);
+  }
+  if (rangeStrategy === 'bump') {
+    // Use the lowest version in the current range
+    return minSatisfyingVersion(allVersions, currentVersion);
+  }
+  // Use the highest version in the current range
+  return maxSatisfyingVersion(allVersions, currentVersion);
+}
diff --git a/lib/manager/npm/lookup/rollback.js b/lib/manager/npm/lookup/rollback.js
new file mode 100644
index 0000000000000000000000000000000000000000..92724ed0d89ffab62e04543d3a6c7e6ae969e868
--- /dev/null
+++ b/lib/manager/npm/lookup/rollback.js
@@ -0,0 +1,46 @@
+const {
+  getMajor,
+  isLessThan,
+  rangify,
+  sortVersions,
+} = require('../../../versioning/semver');
+
+module.exports = {
+  getRollbackUpdate,
+};
+
+function getRollbackUpdate(config, versions) {
+  const { packageFile, depName, currentVersion } = config;
+  const lessThanVersions = versions.filter(version =>
+    isLessThan(version, currentVersion)
+  );
+  // istanbul ignore if
+  if (!lessThanVersions.length) {
+    logger.warn(
+      { packageFile, depName, currentVersion },
+      'Missing version has nothing to roll back to'
+    );
+    return [];
+  }
+  logger.info(
+    { packageFile, depName, currentVersion },
+    `Current version not found - rolling back`
+  );
+  lessThanVersions.sort(sortVersions);
+  const toVersion = lessThanVersions.pop();
+  let fromVersion;
+  const newVersion = rangify(config, currentVersion, fromVersion, toVersion);
+  return [
+    {
+      type: 'rollback',
+      branchName:
+        '{{{branchPrefix}}}rollback-{{{depNameSanitized}}}-{{{newVersionMajor}}}.x',
+      commitMessageAction: 'Roll back',
+      isRollback: true,
+      newVersion,
+      newVersionMajor: getMajor(toVersion),
+      semanticCommitType: 'fix',
+      unpublishable: false,
+    },
+  ];
+}
diff --git a/lib/manager/npm/package.js b/lib/manager/npm/package.js
index cfa17a378b18f1ce15c91701e851ba123a47a89a..f8a420226c2bab67ff41c84eb08916187d3255be 100644
--- a/lib/manager/npm/package.js
+++ b/lib/manager/npm/package.js
@@ -1,8 +1,8 @@
 const npmApi = require('../../datasource/npm');
-const versions = require('./versions');
-const { isValidSemver } = require('../../versioning/semver');
+const lookup = require('./lookup');
+const { isValid } = require('../../versioning/semver');
 const nodeManager = require('../_helpers/node/package');
-const { parseRange } = require('../../versioning/semver');
+const { parseRange } = require('semver-utils');
 
 module.exports = {
   getRangeStrategy,
@@ -60,7 +60,7 @@ async function getPackageUpdates(config) {
     );
     return [];
   }
-  if (!isValidSemver(currentVersion)) {
+  if (!isValid(currentVersion)) {
     results.push({
       depName,
       type: 'warning',
@@ -76,7 +76,7 @@ async function getPackageUpdates(config) {
   );
   const npmDep = await npmApi.getDependency(depName);
   if (npmDep) {
-    results = await versions.determineUpgrades(npmDep, {
+    results = lookup.lookupUpdates(npmDep, {
       ...config,
       rangeStrategy,
     });
diff --git a/lib/manager/npm/versions.js b/lib/manager/npm/versions.js
deleted file mode 100644
index 7f40330b29453acf710a2d28250d5f6c234ca97b..0000000000000000000000000000000000000000
--- a/lib/manager/npm/versions.js
+++ /dev/null
@@ -1,470 +0,0 @@
-const _ = require('lodash');
-const {
-  getMajor,
-  getMinor,
-  getPatch,
-  isGreaterThan,
-  isRange,
-  isStable,
-  isUnstable,
-  isPinnedVersion,
-  matches,
-  maxSatisfyingVersion,
-  minSatisfyingVersion,
-  parseRange,
-  parseVersion,
-  stringifyRange,
-} = require('../../versioning/semver');
-const moment = require('moment');
-
-module.exports = {
-  determineUpgrades,
-  isPastLatest,
-};
-
-function determineUpgrades(npmDep, config) {
-  const dependency = npmDep.name;
-  logger.debug({ dependency }, `determineUpgrades()`);
-  logger.trace({ npmDep, config });
-  const result = {
-    type: 'warning',
-  };
-  const { lockedVersion, rangeStrategy, allowedVersions } = config;
-  const { versions } = npmDep;
-  if (!versions || Object.keys(versions).length === 0) {
-    result.message = `No versions returned from registry for this package`;
-    logger.warn({ dependency }, result.message);
-    return [result];
-  }
-  let versionList = Object.keys(versions);
-  const allUpgrades = {};
-  let { currentVersion } = config;
-  // filter out versions past latest
-  const currentIsPastLatest = isPastLatest(
-    npmDep,
-    minSatisfyingVersion(versionList, currentVersion)
-  );
-  if (currentIsPastLatest) {
-    logger.debug({ name: npmDep.name, currentVersion }, 'currentIsPastLatest');
-  }
-  versionList = versionList.filter(
-    version =>
-      currentIsPastLatest || // if current is past latest then don't filter any
-      config.respectLatest === false || // if user has configured respectLatest to false
-      isPastLatest(npmDep, version) === false // if the version is less than or equal to latest
-  );
-  let rangeOperator;
-  if (config.rangeStrategy === 'bump' && isRange(currentVersion)) {
-    logger.debug({ currentVersion }, 'bumping current range');
-    const parsedRange = parseRange(currentVersion);
-    if (parsedRange && parsedRange.length === 1) {
-      const [range] = parsedRange;
-      if (range.major && range.minor && range.patch) {
-        if (range.operator === '^' || range.operator === '~') {
-          logger.debug('Applying in-range bump');
-          currentVersion = `${range.major}.${range.minor}.${range.patch}`;
-          currentVersion += range.release ? `-${range.release}` : '';
-          rangeOperator = range.operator;
-        } else {
-          logger.debug({ currentVersion }, 'Unsupported range type');
-        }
-      } else {
-        logger.debug({ currentVersion }, 'Range is not fully specified');
-      }
-    } else {
-      logger.debug({ currentVersion }, 'Skipping complex range');
-    }
-  }
-  let fromVersion = currentVersion;
-  // Check for a current range and pin it
-  if (isRange(currentVersion)) {
-    let newVersion;
-    if (
-      rangeStrategy === 'pin' &&
-      lockedVersion &&
-      isPinnedVersion(lockedVersion)
-    ) {
-      newVersion = lockedVersion;
-    } else {
-      // Pin ranges to their maximum satisfying version
-      logger.debug({ dependency }, 'currentVersion is range, not locked');
-      const maxSatisfying = maxSatisfyingVersion(versionList, currentVersion);
-      if (!maxSatisfying) {
-        result.message = `No satisfying version found for existing dependency range "${currentVersion}"`;
-        logger.info(
-          { dependency, currentVersion },
-          `Warning: ${result.message}`
-        );
-        return [result];
-      }
-      logger.debug({ dependency, maxSatisfying });
-      newVersion = maxSatisfying;
-    }
-
-    allUpgrades.pin = {
-      type: 'pin',
-      newVersion,
-      newVersionMajor: getMajor(newVersion),
-    };
-    fromVersion = newVersion;
-  } else if (versionList.indexOf(currentVersion) === -1 && !rangeOperator) {
-    logger.debug({ dependency }, 'Cannot find currentVersion');
-    try {
-      const rollbackVersion = maxSatisfyingVersion(
-        versionList,
-        `<${currentVersion}`
-      );
-      allUpgrades.rollback = {
-        type: 'rollback',
-        newVersion: rollbackVersion,
-        newVersionMajor: getMajor(rollbackVersion),
-        semanticCommitType: 'fix',
-        commitMessageAction: 'Roll back',
-        branchName:
-          '{{{branchPrefix}}}rollback-{{{depNameSanitized}}}-{{{newVersionMajor}}}.x',
-      };
-    } catch (err) /* istanbul ignore next */ {
-      logger.info(
-        { dependency, currentVersion },
-        'Warning: current version is missing from npm registry and cannot roll back'
-      );
-    }
-  }
-  _(versionList)
-    // Filter out older versions as we can't upgrade to those
-    .filter(version => isGreaterThan(version, fromVersion))
-    // fillter out non-allowed versions if preference is set
-    .reject(version => allowedVersions && !matches(version, allowedVersions))
-    // Ignore unstable versions, unless the current version is unstable
-    .reject(
-      version =>
-        config.ignoreUnstable && isStable(fromVersion) && isUnstable(version)
-    )
-    // Do not jump to a new major unstable just because the current is unstable
-    .reject(
-      version =>
-        config.ignoreUnstable &&
-        isUnstable(version) &&
-        getMajor(version) > getMajor(fromVersion)
-    )
-    // Loop through all possible versions
-    .forEach(newVersion => {
-      // Group by major versions
-      const newVersionMajor = getMajor(newVersion);
-      const newVersionMinor = getMinor(newVersion);
-      const hasPatchOnlyAutomerge =
-        config.patch &&
-        config.patch.automerge === true &&
-        (config.minor && config.minor.automerge !== true);
-      let type;
-      if (newVersionMajor > getMajor(fromVersion)) {
-        type = 'major';
-      } else if (
-        newVersionMinor === getMinor(fromVersion) &&
-        (config.separateMinorPatch || hasPatchOnlyAutomerge)
-      ) {
-        // Only use patch if configured to
-        type = 'patch';
-      } else {
-        type = 'minor';
-      }
-      let upgradeKey;
-      if (
-        !config.separateMajorMinor ||
-        config.groupName ||
-        config.major.automerge === true
-      ) {
-        // If we're not separating releases then we use a common lookup key
-        upgradeKey = 'latest';
-      } else if (!config.separateMultipleMajor && type === 'major') {
-        upgradeKey = 'major';
-      } else if (type === 'patch') {
-        upgradeKey = `{{{newVersionMajor}}}.{{{newVersionMinor}}}`;
-      } else {
-        // Use major version as lookup key
-        upgradeKey = newVersionMajor;
-      }
-      // Save this, if it's a new major version or greater than the previous greatest
-      if (
-        !allUpgrades[upgradeKey] ||
-        isGreaterThan(newVersion, allUpgrades[upgradeKey].newVersion)
-      ) {
-        const toVersion = newVersion;
-        allUpgrades[upgradeKey] = {
-          type,
-          newVersion,
-          newVersionMajor,
-          newVersionMinor,
-          fromVersion,
-          toVersion,
-        };
-      }
-    });
-  // Return only the values - we don't need the keys anymore
-  let upgrades = Object.keys(allUpgrades).map(key => allUpgrades[key]);
-  for (const upgrade of upgrades) {
-    const version = versions[upgrade.newVersion];
-    const elapsed = version ? moment().diff(moment(version.time), 'days') : 999;
-    upgrade.unpublishable = elapsed > 0;
-  }
-
-  // Return now if array is empty, or we can keep pinned version upgrades
-  if (
-    upgrades.length === 0 ||
-    config.rangeStrategy === 'pin' ||
-    !isRange(currentVersion)
-  ) {
-    return rangeOperator
-      ? upgrades.map(upgrade => ({
-          ...upgrade,
-          newVersion: `${rangeOperator}${upgrade.newVersion}`,
-          isRange: true,
-        }))
-      : upgrades;
-  }
-
-  logger.debug({ dependency }, 'User wants ranges - filtering out pins');
-  upgrades = upgrades.filter(upgrade => upgrade.type !== 'pin');
-
-  // Return empty if all results were pins
-  if (!upgrades.length) {
-    logger.debug({ dependency }, 'No upgrades left - returning');
-    return [];
-  }
-
-  // Check if it's a range type we support
-  const semverParsed = parseRange(currentVersion);
-  // Check the "last" part, which is also the first and only if it's a simple semver
-  const [lastSemver] = semverParsed.slice(-1);
-  const secondLastSemver = semverParsed[semverParsed.length - 2];
-  if (semverParsed.length > 1) {
-    if (lastSemver.operator === '<' || lastSemver.operator === '<=') {
-      logger.debug({ dependency }, 'Found less than range');
-    } else if (secondLastSemver.operator === '||') {
-      logger.debug({ dependency }, 'Found an OR range');
-    } else if (secondLastSemver.operator === '-') {
-      logger.info(
-        { dependency, currentVersion, upgrades, semverParsed },
-        'Found a hyphen range'
-      );
-    } else {
-      // We don't know how to support complex semver ranges, so don't upgrade
-      result.message = `Complex semver ranges such as "${currentVersion}" are not yet supported so will be skipped`;
-      logger.info(
-        { dependency, upgrades, semverParsed },
-        'Semver warning: ' + result.message
-      );
-      return [result];
-    }
-  }
-  // Loop through all upgrades and convert to ranges
-  const rangedUpgrades = _(upgrades)
-    .map(upgrade => ({ ...upgrade, ...{ isRange: true } }))
-    .map(upgrade => {
-      const { major, minor } = parseVersion(upgrade.newVersion);
-      const canReplace = config.rangeStrategy !== 'widen';
-      const forceReplace = config.rangeStrategy === 'replace';
-      const canWiden = config.rangeStrategy !== 'replace';
-      const forceWiden = config.rangeStrategy === 'widen';
-      if (
-        lastSemver.operator === '~' &&
-        canReplace &&
-        (semverParsed.length === 1 || forceReplace)
-      ) {
-        // Utilise that a.b is the same as ~a.b.0
-        const minSatisfying = minSatisfyingVersion(
-          versionList,
-          `${major}.${minor}`
-        );
-        // Add a tilde before that version number
-        return { ...upgrade, ...{ newVersion: `~${minSatisfying}` } };
-      } else if (
-        lastSemver.operator === '~' &&
-        canWiden &&
-        (semverParsed.length > 1 || forceWiden)
-      ) {
-        // Utilise that a.b is the same as ~a.b.0
-        const minSatisfying = minSatisfyingVersion(
-          versionList,
-          `${major}.${minor}`
-        );
-        // Add a tilde before that version number
-        const newVersion = `${currentVersion} || ~${minSatisfying}`;
-        return {
-          ...upgrade,
-          newVersion,
-        };
-      } else if (
-        lastSemver.operator === '^' &&
-        canReplace &&
-        (semverParsed.length === 1 || forceReplace)
-      ) {
-        let newVersion;
-        // Special case where major and minor are 0
-        if (major === '0') {
-          if (minor === '0') {
-            newVersion = `^${upgrade.newVersion}`;
-          } else {
-            // If version is < 1, then semver treats ^ same as ~
-            const newRange = `${major}.${minor}`;
-            const minSatisfying = minSatisfyingVersion(versionList, newRange);
-            // Add in the caret
-            newVersion = `^${minSatisfying}`;
-          }
-        } else if (major === `${getMajor(fromVersion)}`) {
-          newVersion = `^${upgrade.newVersion}`;
-        } else {
-          const newRange = `${major}`;
-          const minSatisfying = minSatisfyingVersion(versionList, newRange);
-          // Add in the caret
-          newVersion = `^${minSatisfying}`;
-        }
-        return { ...upgrade, newVersion };
-      } else if (
-        lastSemver.operator === '^' &&
-        canWiden &&
-        (semverParsed.length > 1 || forceWiden)
-      ) {
-        // If version is < 1, then semver treats ^ same as ~
-        const newRange = major === '0' ? `${major}.${minor}` : `${major}`;
-        const minSatisfying = minSatisfyingVersion(versionList, newRange);
-        // Add in the caret
-        const newVersion = `${currentVersion} || ^${minSatisfying}`;
-        return {
-          ...upgrade,
-          newVersion,
-        };
-      } else if (lastSemver.operator === '<=') {
-        const minorZero = !lastSemver.minor || lastSemver.minor === '0';
-        const patchZero = !lastSemver.patch || lastSemver.patch === '0';
-        const newRange = [...semverParsed];
-        if (minorZero && patchZero) {
-          logger.debug({ dependency }, 'Found a less than major');
-          newRange[newRange.length - 1].major = String(
-            upgrade.newVersionMajor + 1
-          );
-        } else if (patchZero) {
-          logger.debug({ dependency }, 'Found a less than minor');
-          newRange[newRange.length - 1].major = String(upgrade.newVersionMajor);
-          newRange[newRange.length - 1].minor = String(
-            upgrade.newVersionMinor + 1
-          );
-        } else {
-          logger.debug({ dependency }, 'Found a less than full semver');
-          newRange[newRange.length - 1].major = String(upgrade.newVersionMajor);
-          newRange[newRange.length - 1].minor = String(upgrade.newVersionMinor);
-          newRange[newRange.length - 1].patch = String(
-            getPatch(upgrade.newVersion)
-          );
-        }
-        let newVersion = stringifyRange(newRange);
-        newVersion = fixRange(newVersion, lastSemver, currentVersion);
-        return { ...upgrade, newVersion };
-      } else if (lastSemver.operator === '<') {
-        const minorZero = !lastSemver.minor || lastSemver.minor === '0';
-        const patchZero = !lastSemver.patch || lastSemver.patch === '0';
-        const newRange = [...semverParsed];
-        if (minorZero && patchZero) {
-          logger.debug({ dependency }, 'Found a less than major');
-          newRange[newRange.length - 1].major = String(
-            upgrade.newVersionMajor + 1
-          );
-        } else if (patchZero) {
-          logger.debug({ dependency }, 'Found a less than minor');
-          newRange[newRange.length - 1].major = String(upgrade.newVersionMajor);
-          newRange[newRange.length - 1].minor = String(
-            upgrade.newVersionMinor + 1
-          );
-        } else {
-          logger.debug({ dependency }, 'Found full semver minor');
-          newRange[newRange.length - 1].major = String(upgrade.newVersionMajor);
-          newRange[newRange.length - 1].minor = String(upgrade.newVersionMinor);
-          newRange[newRange.length - 1].patch = String(
-            getPatch(upgrade.newVersion) + 1
-          );
-        }
-        let newVersion = stringifyRange(newRange);
-        newVersion = fixRange(newVersion, lastSemver, currentVersion);
-        return { ...upgrade, newVersion };
-      } else if (lastSemver.minor === undefined) {
-        // Example: 1
-        const newRange = [...semverParsed];
-        logger.debug({ dependency }, 'Found a standalone major');
-        newRange[newRange.length - 1].major = String(upgrade.newVersionMajor);
-        let newVersion;
-        if (secondLastSemver && secondLastSemver.operator === '||') {
-          newVersion = `${currentVersion} || ${upgrade.newVersionMajor}`;
-        } else {
-          newVersion = stringifyRange(newRange);
-          // Fixes a bug with stringifyRange
-          newVersion = newVersion.replace(/\.0/g, '');
-        }
-        return { ...upgrade, newVersion };
-      } else if (lastSemver.minor === 'x') {
-        // Example: 1.x
-        const newRange = [...semverParsed];
-        logger.debug({ dependency }, 'Found a .x');
-        newRange[newRange.length - 1].major = String(upgrade.newVersionMajor);
-        let newVersion;
-        if (secondLastSemver && secondLastSemver.operator === '||') {
-          newVersion = `${currentVersion} || ${upgrade.newVersionMajor}.x`;
-        } else {
-          newVersion = stringifyRange(newRange);
-          // Fixes a bug with stringifyRange
-          newVersion = newVersion.replace(/\.0/g, '');
-        }
-        return { ...upgrade, newVersion };
-      } else if (lastSemver.patch === undefined) {
-        // Example: 1.2
-        return { ...upgrade, ...{ newVersion: `${major}.${minor}` } };
-      } else if (lastSemver.patch === 'x' && semverParsed.length === 1) {
-        // Example: 1.2.x
-        return { ...upgrade, ...{ newVersion: `${major}.${minor}.x` } };
-      }
-      // istanbul ignore next
-      result.message = `The current semver range "${currentVersion}" is not supported so won't ever be upgraded`;
-      // istanbul ignore next
-      logger.warn({ dependency }, result.message);
-      // istanbul ignore next
-      return null;
-    })
-    .compact()
-    .value();
-  // istanbul ignore if
-  if (result.message) {
-    // There must have been an error converting to ranges
-    return [result];
-  }
-  return rangedUpgrades;
-}
-
-function fixRange(version, lastSemver, currentVersion) {
-  let newVersion = version;
-  if (!lastSemver.patch) {
-    newVersion = newVersion.replace(/\.0$/, '');
-  }
-  if (!currentVersion.includes('< ')) {
-    newVersion = newVersion.replace(/< /g, '<');
-  }
-  if (!currentVersion.includes('> ')) {
-    newVersion = newVersion.replace(/> /g, '>');
-  }
-  if (!currentVersion.includes('>= ')) {
-    newVersion = newVersion.replace(/>= /g, '>=');
-  }
-  if (!currentVersion.includes('<= ')) {
-    newVersion = newVersion.replace(/<= /g, '<=');
-  }
-  return newVersion;
-}
-
-function isPastLatest(npmDep, version) {
-  if (!version) {
-    return false;
-  }
-  if (npmDep['dist-tags'] && npmDep['dist-tags'].latest) {
-    return isGreaterThan(version, npmDep['dist-tags'].latest);
-  }
-  logger.warn(`No dist-tags.latest for ${npmDep.name}`);
-  return false;
-}
diff --git a/lib/util/package-rules.js b/lib/util/package-rules.js
index 8d4e8312d3d81d2acdf5fcf716dcc7e5946814fd..647bef919c69a031f268b4edf79282c4a53ddb08 100644
--- a/lib/util/package-rules.js
+++ b/lib/util/package-rules.js
@@ -1,6 +1,6 @@
 const minimatch = require('minimatch');
 
-const { intersectsSemver } = require('../versioning/semver');
+const { intersects } = require('../versioning/semver');
 const { mergeChildConfig } = require('../config');
 
 module.exports = {
@@ -94,7 +94,7 @@ function applyPackageRules(inputConfig) {
       positiveMatch = positiveMatch || !isMatch;
     }
     if (matchCurrentVersion) {
-      const isMatch = intersectsSemver(currentVersion, matchCurrentVersion);
+      const isMatch = intersects(currentVersion, matchCurrentVersion);
       positiveMatch = positiveMatch || isMatch;
       negativeMatch = negativeMatch || !isMatch;
     }
diff --git a/lib/versioning/semver/index.js b/lib/versioning/semver/index.js
index 444e1020c8a3aeae3ad2c20b9bc1d46de751c4ad..0a3593f2d40fe3f3fda383b93ea73fba801a6ef3 100644
--- a/lib/versioning/semver/index.js
+++ b/lib/versioning/semver/index.js
@@ -1,22 +1,18 @@
 const semver = require('semver');
 const stable = require('semver-stable');
-const semverUtils = require('semver-utils');
+const { rangify } = require('./range');
 
 const { is: isStable } = stable;
-const isUnstable = input => !isStable(input);
-
-const { parseRange, parse: parseVersion, stringifyRange } = semverUtils;
 
 const {
   compare: sortVersions,
-  gt: isGreaterThan,
-  intersects: intersectsSemver,
+  intersects,
   maxSatisfying: maxSatisfyingVersion,
   minSatisfying: minSatisfyingVersion,
   minor: getMinor,
-  patch: getPatch,
   satisfies: matches,
   valid: isPinnedVersion,
+  validRange,
 } = semver;
 
 const padRange = range => range + '.0'.repeat(3 - range.split('.').length);
@@ -26,27 +22,30 @@ const getMajor = input => {
   return semver.major(version);
 };
 
-const isRange = input => isValidSemver(input) && !isPinnedVersion(input);
+const isRange = input => isValid(input) && !isPinnedVersion(input);
 
 // If this is left as an alias, inputs like "17.04.0" throw errors
-const isValidSemver = input => semver.validRange(input);
+const isValid = input => validRange(input);
+
+const isLessThan = (version, base) =>
+  isPinnedVersion(base) ? semver.lt(version, base) : semver.ltr(version, base);
+
+const isGreaterThan = (version, base) =>
+  isPinnedVersion(base) ? semver.gt(version, base) : semver.gtr(version, base);
 
 module.exports = {
   getMajor,
   getMinor,
-  getPatch,
-  intersectsSemver,
+  intersects,
   isGreaterThan,
+  isLessThan,
+  isPinnedVersion,
   isRange,
   isStable,
-  isUnstable,
-  isValidSemver,
-  isPinnedVersion,
+  isValid,
   matches,
   maxSatisfyingVersion,
   minSatisfyingVersion,
-  parseRange,
-  parseVersion,
+  rangify,
   sortVersions,
-  stringifyRange,
 };
diff --git a/lib/versioning/semver/range.js b/lib/versioning/semver/range.js
new file mode 100644
index 0000000000000000000000000000000000000000..e55ade1c2437f38425b1253cb09a810489f384dc
--- /dev/null
+++ b/lib/versioning/semver/range.js
@@ -0,0 +1,117 @@
+const { inc: increment, major, minor, valid } = require('semver');
+const { parseRange } = require('semver-utils');
+
+module.exports = {
+  rangify,
+};
+
+function rangify(config, currentVersion, fromVersion, toVersion) {
+  const { rangeStrategy } = config;
+  if (rangeStrategy === 'pin' || valid(currentVersion)) {
+    return toVersion;
+  }
+  const parsedRange = parseRange(currentVersion);
+  const element = parsedRange[parsedRange.length - 1];
+  if (rangeStrategy === 'widen') {
+    const newVersion = rangify(
+      { ...config, rangeStrategy: 'replace' },
+      currentVersion,
+      fromVersion,
+      toVersion
+    );
+    if (element.operator && element.operator.startsWith('<')) {
+      // TODO fix this
+      const splitCurrent = currentVersion.split(element.operator);
+      splitCurrent.pop();
+      return splitCurrent.join(element.operator) + newVersion;
+    }
+    if (parsedRange.length > 1) {
+      const previousElement = parsedRange[parsedRange.length - 2];
+      if (previousElement.operator === '-') {
+        const splitCurrent = currentVersion.split('-');
+        splitCurrent.pop();
+        return splitCurrent.join('-') + '- ' + newVersion;
+      }
+      if (element.operator && element.operator.startsWith('>')) {
+        logger.warn(`Complex ranges ending in greater than are not supported`);
+        return null;
+      }
+    }
+    return `${currentVersion} || ${newVersion}`;
+  }
+  // console.log(range);
+  // Simple range
+  if (config.rangeStrategy === 'bump') {
+    if (parsedRange.length === 1) {
+      if (element.operator === '^') {
+        return `^${toVersion}`;
+      }
+      if (element.operator === '~') {
+        return `~${toVersion}`;
+      }
+    }
+    logger.warn(
+      'Unsupported range type for rangeStrategy=bump: ' + currentVersion
+    );
+    return null;
+  }
+  if (element.operator === '^') {
+    if (fromVersion && major(toVersion) === major(fromVersion)) {
+      if (major(toVersion) === 0) {
+        if (minor(toVersion) === 0) {
+          return `^${toVersion}`;
+        }
+        return `^${major(toVersion)}.${minor(toVersion)}.0`;
+      }
+      return `^${toVersion}`;
+    }
+    return `^${major(toVersion)}.0.0`;
+  }
+  if (element.operator === '~') {
+    return `~${major(toVersion)}.${minor(toVersion)}.0`;
+  }
+  if (element.operator === '<=') {
+    let res;
+    if (element.patch) {
+      res = `<=${toVersion}`;
+    } else if (element.minor) {
+      res = `<=${major(toVersion)}.${minor(toVersion)}`;
+    } else {
+      res = `<=${major(toVersion)}`;
+    }
+    if (currentVersion.includes('<= ')) {
+      res = res.replace('<=', '<= ');
+    }
+    return res;
+  }
+  if (element.operator === '<') {
+    let res;
+    if (currentVersion.endsWith('.0.0')) {
+      const newMajor = major(toVersion) + 1;
+      res = `<${newMajor}.0.0`;
+    } else if (element.patch) {
+      res = `<${increment(toVersion, 'patch')}`;
+    } else if (element.minor) {
+      res = `<${major(toVersion)}.${minor(toVersion) + 1}`;
+    } else {
+      res = `<${major(toVersion) + 1}`;
+    }
+    if (currentVersion.includes('< ')) {
+      res = res.replace('<', '< ');
+    }
+    return res;
+  }
+  if (!element.operator) {
+    if (element.minor) {
+      if (element.minor === 'x') {
+        return `${major(toVersion)}.x`;
+      }
+      if (element.patch === 'x') {
+        return `${major(toVersion)}.${minor(toVersion)}.x`;
+      }
+      return `${major(toVersion)}.${minor(toVersion)}`;
+    }
+    return `${major(toVersion)}`;
+  }
+  return toVersion;
+}
diff --git a/test/manager/npm/__snapshots__/versions.spec.js.snap b/test/manager/npm/__snapshots__/lookup.spec.js.snap
similarity index 64%
rename from test/manager/npm/__snapshots__/versions.spec.js.snap
rename to test/manager/npm/__snapshots__/lookup.spec.js.snap
index bcf9a956ecd575c1996133cded28826d59bdd065..dbff4230ba8f5d387b7059b8dc21aade96a97cf4 100644
--- a/test/manager/npm/__snapshots__/versions.spec.js.snap
+++ b/test/manager/npm/__snapshots__/lookup.spec.js.snap
@@ -1,8 +1,9 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) disables major release separation (major) 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() disables major release separation (major) 1`] = `
 Array [
   Object {
+    "isPin": true,
     "newVersion": "0.4.4",
     "newVersionMajor": 0,
     "type": "pin",
@@ -20,7 +21,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) disables major release separation (minor) 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() disables major release separation (minor) 1`] = `
 Array [
   Object {
     "fromVersion": "1.0.0",
@@ -34,7 +35,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) handles prerelease jumps 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() handles prerelease jumps 1`] = `
 Array [
   Object {
     "fromVersion": "2.9.0-rc",
@@ -49,9 +50,10 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) ignores pinning for ranges when other upgrade exists 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() ignores pinning for ranges when other upgrade exists 1`] = `
 Array [
   Object {
+    "isPin": true,
     "newVersion": "0.9.7",
     "newVersionMajor": 0,
     "type": "pin",
@@ -69,9 +71,10 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) pins minor ranged versions 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() pins minor ranged versions 1`] = `
 Array [
   Object {
+    "isPin": true,
     "newVersion": "1.4.1",
     "newVersionMajor": 1,
     "type": "pin",
@@ -80,13 +83,13 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) rejects complex range in-range updates 1`] = `Array []`;
+exports[`manager/npm/lookup .lookupUpdates() rejects complex range in-range updates 1`] = `Array []`;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) rejects in-range unsupported operator 1`] = `Array []`;
+exports[`manager/npm/lookup .lookupUpdates() rejects in-range unsupported operator 1`] = `Array []`;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) rejects non-fully specified in-range updates 1`] = `Array []`;
+exports[`manager/npm/lookup .lookupUpdates() rejects non-fully specified in-range updates 1`] = `Array []`;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) rejects non-range in-range updates 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() rejects non-range in-range updates 1`] = `
 Array [
   Object {
     "fromVersion": "1.0.0",
@@ -100,16 +103,9 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) rejects reverse ordered less than greater than 1`] = `
-Array [
-  Object {
-    "message": "Complex semver ranges such as \\"<= 0.8.0 >= 0.5.0\\" are not yet supported so will be skipped",
-    "type": "warning",
-  },
-]
-`;
+exports[`manager/npm/lookup .lookupUpdates() rejects reverse ordered less than greater than 1`] = `Array []`;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) replaces major complex ranged versions if configured 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() replaces major complex ranged versions if configured 1`] = `
 Array [
   Object {
     "fromVersion": "2.7.0",
@@ -124,7 +120,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) replaces minor complex ranged versions if configured 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() replaces minor complex ranged versions if configured 1`] = `
 Array [
   Object {
     "fromVersion": "1.3.0",
@@ -139,22 +135,22 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) return warning if empty versions 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() return warning if empty versions 1`] = `
 Object {
   "message": "No versions returned from registry for this package",
   "type": "warning",
 }
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) return warning if null versions 1`] = `
-Object {
-  "message": "No versions returned from registry for this package",
-  "type": "warning",
-}
-`;
-
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) returns both updates if automerging minor 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() returns both updates if automerging minor 1`] = `
 Array [
+  Object {
+    "isPin": true,
+    "newVersion": "0.4.4",
+    "newVersionMajor": 0,
+    "type": "pin",
+    "unpublishable": false,
+  },
   Object {
     "fromVersion": "0.4.4",
     "newVersion": "0.9.7",
@@ -164,12 +160,6 @@ Array [
     "type": "minor",
     "unpublishable": false,
   },
-  Object {
-    "newVersion": "0.4.4",
-    "newVersionMajor": 0,
-    "type": "pin",
-    "unpublishable": false,
-  },
   Object {
     "fromVersion": "0.4.4",
     "newVersion": "1.4.1",
@@ -182,7 +172,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) returns minor update if automerging both patch and minor 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() returns minor update if automerging both patch and minor 1`] = `
 Array [
   Object {
     "fromVersion": "0.9.0",
@@ -205,7 +195,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) returns minor update if separate patches not configured 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() returns minor update if separate patches not configured 1`] = `
 Array [
   Object {
     "fromVersion": "0.9.0",
@@ -228,9 +218,24 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) returns only one update if automerging major 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() returns only one update if automerging 1`] = `
 Array [
   Object {
+    "fromVersion": "0.4.0",
+    "newVersion": "1.4.1",
+    "newVersionMajor": 1,
+    "newVersionMinor": 4,
+    "toVersion": "1.4.1",
+    "type": "major",
+    "unpublishable": false,
+  },
+]
+`;
+
+exports[`manager/npm/lookup .lookupUpdates() returns only one update if automerging major 1`] = `
+Array [
+  Object {
+    "isPin": true,
     "newVersion": "0.4.4",
     "newVersionMajor": 0,
     "type": "pin",
@@ -248,9 +253,10 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) returns only one update if grouping 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() returns only one update if grouping 1`] = `
 Array [
   Object {
+    "isPin": true,
     "newVersion": "0.4.4",
     "newVersionMajor": 0,
     "type": "pin",
@@ -268,24 +274,24 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) returns patch minor and major 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() returns patch minor and major 1`] = `
 Array [
   Object {
     "fromVersion": "0.8.0",
-    "newVersion": "0.9.7",
+    "newVersion": "0.8.12",
     "newVersionMajor": 0,
-    "newVersionMinor": 9,
-    "toVersion": "0.9.7",
-    "type": "minor",
+    "newVersionMinor": 8,
+    "toVersion": "0.8.12",
+    "type": "patch",
     "unpublishable": false,
   },
   Object {
     "fromVersion": "0.8.0",
-    "newVersion": "0.8.12",
+    "newVersion": "0.9.7",
     "newVersionMajor": 0,
-    "newVersionMinor": 8,
-    "toVersion": "0.8.12",
-    "type": "patch",
+    "newVersionMinor": 9,
+    "toVersion": "0.9.7",
+    "type": "minor",
     "unpublishable": false,
   },
   Object {
@@ -300,7 +306,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) returns patch update if automerging patch 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() returns patch update if automerging patch 1`] = `
 Array [
   Object {
     "fromVersion": "0.9.0",
@@ -323,7 +329,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) returns patch update if separateMinorPatch 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() returns patch update if separateMinorPatch 1`] = `
 Array [
   Object {
     "fromVersion": "0.9.0",
@@ -346,16 +352,37 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) returns warning if range not found 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() returns rollback if range not found 1`] = `
 Array [
   Object {
-    "message": "No satisfying version found for existing dependency range \\"^8.4.0\\"",
-    "type": "warning",
+    "branchName": "{{{branchPrefix}}}rollback-{{{depNameSanitized}}}-{{{newVersionMajor}}}.x",
+    "commitMessageAction": "Roll back",
+    "isRollback": true,
+    "newVersion": "^2.0.0",
+    "newVersionMajor": 2,
+    "semanticCommitType": "fix",
+    "type": "rollback",
+    "unpublishable": false,
   },
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) should allow unstable versions if the current version is unstable 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() should allow unstable versions if ignoreUnstable is false 1`] = `
+Array [
+  Object {
+    "branchName": "{{{branchPrefix}}}rollback-{{{depNameSanitized}}}-{{{newVersionMajor}}}.x",
+    "commitMessageAction": "Roll back",
+    "isRollback": true,
+    "newVersion": "1.0.0-beta",
+    "newVersionMajor": 1,
+    "semanticCommitType": "fix",
+    "type": "rollback",
+    "unpublishable": false,
+  },
+]
+`;
+
+exports[`manager/npm/lookup .lookupUpdates() should allow unstable versions if the current version is unstable 1`] = `
 Array [
   Object {
     "fromVersion": "1.0.0-beta",
@@ -369,10 +396,25 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) should downgrade from missing versions 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() should allow unstable versions if the ignoreUnstable=false 1`] = `
+Array [
+  Object {
+    "fromVersion": "1.0.0",
+    "newVersion": "1.1.0-beta",
+    "newVersionMajor": 1,
+    "newVersionMinor": 1,
+    "toVersion": "1.1.0-beta",
+    "type": "minor",
+    "unpublishable": false,
+  },
+]
+`;
+
+exports[`manager/npm/lookup .lookupUpdates() should downgrade from missing versions 1`] = `
 Object {
   "branchName": "{{{branchPrefix}}}rollback-{{{depNameSanitized}}}-{{{newVersionMajor}}}.x",
   "commitMessageAction": "Roll back",
+  "isRollback": true,
   "newVersion": "1.16.0",
   "newVersionMajor": 1,
   "semanticCommitType": "fix",
@@ -381,7 +423,7 @@ Object {
 }
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) should treat zero zero caret ranges as pinned 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() should treat zero zero caret ranges as pinned 1`] = `
 Array [
   Object {
     "fromVersion": "0.0.34",
@@ -396,7 +438,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) supports > latest versions if configured 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() supports > latest versions if configured 1`] = `
 Array [
   Object {
     "fromVersion": "1.4.1",
@@ -410,7 +452,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) supports complex major hyphen ranges 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() supports complex major hyphen ranges 1`] = `
 Array [
   Object {
     "fromVersion": "2.7.0",
@@ -425,7 +467,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) supports complex major ranges 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() supports complex major ranges 1`] = `
 Array [
   Object {
     "fromVersion": "2.7.0",
@@ -440,7 +482,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) supports complex ranges 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() supports complex ranges 1`] = `
 Object {
   "fromVersion": "0.8.12",
   "isRange": true,
@@ -453,7 +495,7 @@ Object {
 }
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) supports complex tilde ranges 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() supports complex tilde ranges 1`] = `
 Array [
   Object {
     "fromVersion": "1.3.0",
@@ -468,18 +510,22 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) supports future versions if already future 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() supports in-range caret updates 1`] = `
 Array [
   Object {
-    "newVersion": "2.0.3",
-    "newVersionMajor": 2,
-    "type": "pin",
+    "fromVersion": "1.0.0",
+    "isRange": true,
+    "newVersion": "^1.4.1",
+    "newVersionMajor": 1,
+    "newVersionMinor": 4,
+    "toVersion": "1.4.1",
+    "type": "minor",
     "unpublishable": false,
   },
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) supports in-range updates 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() supports in-range tilde updates 1`] = `
 Array [
   Object {
     "fromVersion": "1.0.0",
@@ -494,8 +540,15 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) supports minor and major upgrades for ranged versions 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() supports minor and major upgrades for ranged versions 1`] = `
 Array [
+  Object {
+    "isPin": true,
+    "newVersion": "0.4.4",
+    "newVersionMajor": 0,
+    "type": "pin",
+    "unpublishable": false,
+  },
   Object {
     "fromVersion": "0.4.4",
     "newVersion": "0.9.7",
@@ -505,12 +558,6 @@ Array [
     "type": "minor",
     "unpublishable": false,
   },
-  Object {
-    "newVersion": "0.4.4",
-    "newVersionMajor": 0,
-    "type": "pin",
-    "unpublishable": false,
-  },
   Object {
     "fromVersion": "0.4.4",
     "newVersion": "1.4.1",
@@ -523,8 +570,15 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) supports minor and major upgrades for tilde ranges 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() supports minor and major upgrades for tilde ranges 1`] = `
 Array [
+  Object {
+    "isPin": true,
+    "newVersion": "0.4.4",
+    "newVersionMajor": 0,
+    "type": "pin",
+    "unpublishable": false,
+  },
   Object {
     "fromVersion": "0.4.4",
     "newVersion": "0.9.7",
@@ -534,12 +588,6 @@ Array [
     "type": "minor",
     "unpublishable": false,
   },
-  Object {
-    "newVersion": "0.4.4",
-    "newVersionMajor": 0,
-    "type": "pin",
-    "unpublishable": false,
-  },
   Object {
     "fromVersion": "0.4.4",
     "newVersion": "1.4.1",
@@ -552,7 +600,22 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades .x major ranges without pinning 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades .x complex minor ranges without pinning 1`] = `
+Array [
+  Object {
+    "fromVersion": "1.3.0",
+    "isRange": true,
+    "newVersion": "1.2.x - 1.4.x",
+    "newVersionMajor": 1,
+    "newVersionMinor": 4,
+    "toVersion": "1.4.1",
+    "type": "minor",
+    "unpublishable": false,
+  },
+]
+`;
+
+exports[`manager/npm/lookup .lookupUpdates() upgrades .x major ranges without pinning 1`] = `
 Array [
   Object {
     "fromVersion": "0.9.7",
@@ -567,8 +630,15 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades .x minor ranges 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades .x minor ranges 1`] = `
 Array [
+  Object {
+    "isPin": true,
+    "newVersion": "1.3.0",
+    "newVersionMajor": 1,
+    "type": "pin",
+    "unpublishable": false,
+  },
   Object {
     "fromVersion": "1.3.0",
     "newVersion": "1.4.1",
@@ -578,21 +648,45 @@ Array [
     "type": "minor",
     "unpublishable": false,
   },
+]
+`;
+
+exports[`manager/npm/lookup .lookupUpdates() upgrades .x minor ranges without pinning 1`] = `
+Array [
   Object {
-    "newVersion": "1.3.0",
+    "fromVersion": "1.3.0",
+    "isRange": true,
+    "newVersion": "1.4.x",
     "newVersionMajor": 1,
-    "type": "pin",
+    "newVersionMinor": 4,
+    "toVersion": "1.4.1",
+    "type": "minor",
     "unpublishable": false,
   },
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades .x minor ranges without pinning 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades less than equal major ranges 1`] = `
+Array [
+  Object {
+    "fromVersion": "1.4.1",
+    "isRange": true,
+    "newVersion": "<= 2",
+    "newVersionMajor": 2,
+    "newVersionMinor": 0,
+    "toVersion": "2.0.3",
+    "type": "major",
+    "unpublishable": false,
+  },
+]
+`;
+
+exports[`manager/npm/lookup .lookupUpdates() upgrades less than equal minor ranges 1`] = `
 Array [
   Object {
     "fromVersion": "1.3.0",
     "isRange": true,
-    "newVersion": "1.4.x",
+    "newVersion": "<= 1.4",
     "newVersionMajor": 1,
     "newVersionMinor": 4,
     "toVersion": "1.4.1",
@@ -602,7 +696,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades less than equal ranges without pinning 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades less than equal ranges without pinning 1`] = `
 Array [
   Object {
     "fromVersion": "0.7.2",
@@ -627,7 +721,22 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades less than ranges without pinning 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades less than major ranges 1`] = `
+Array [
+  Object {
+    "fromVersion": "0.9.7",
+    "isRange": true,
+    "newVersion": "< 2",
+    "newVersionMajor": 1,
+    "newVersionMinor": 4,
+    "toVersion": "1.4.1",
+    "type": "major",
+    "unpublishable": false,
+  },
+]
+`;
+
+exports[`manager/npm/lookup .lookupUpdates() upgrades less than ranges without pinning 1`] = `
 Array [
   Object {
     "fromVersion": "0.7.1",
@@ -652,7 +761,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades major greater than less than ranges without pinning 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades major greater than less than ranges without pinning 1`] = `
 Array [
   Object {
     "fromVersion": "0.9.7",
@@ -667,12 +776,12 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades major less than equal ranges 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades major less than equal ranges 1`] = `
 Array [
   Object {
     "fromVersion": "1.0.0",
     "isRange": true,
-    "newVersion": "<= 2.0.0",
+    "newVersion": "<= 1.4.1",
     "newVersionMajor": 1,
     "newVersionMinor": 4,
     "toVersion": "1.4.1",
@@ -682,7 +791,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades major less than ranges without pinning 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades major less than ranges without pinning 1`] = `
 Array [
   Object {
     "fromVersion": "0.9.7",
@@ -697,12 +806,12 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades minor greater than less than equals ranges without pinning 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades minor greater than less than equals ranges without pinning 1`] = `
 Array [
   Object {
     "fromVersion": "0.8.0",
     "isRange": true,
-    "newVersion": ">= 0.5.0 <= 0.10.0",
+    "newVersion": ">= 0.5.0 <= 0.9.7",
     "newVersionMajor": 0,
     "newVersionMinor": 9,
     "toVersion": "0.9.7",
@@ -712,7 +821,7 @@ Array [
   Object {
     "fromVersion": "0.8.0",
     "isRange": true,
-    "newVersion": ">= 0.5.0 <= 1.5.0",
+    "newVersion": ">= 0.5.0 <= 1.4.1",
     "newVersionMajor": 1,
     "newVersionMinor": 4,
     "toVersion": "1.4.1",
@@ -722,7 +831,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades minor greater than less than ranges without pinning 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades minor greater than less than ranges without pinning 1`] = `
 Array [
   Object {
     "fromVersion": "0.7.2",
@@ -747,8 +856,15 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades minor ranged versions 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades minor ranged versions 1`] = `
 Array [
+  Object {
+    "isPin": true,
+    "newVersion": "1.0.1",
+    "newVersionMajor": 1,
+    "type": "pin",
+    "unpublishable": false,
+  },
   Object {
     "fromVersion": "1.0.1",
     "newVersion": "1.4.1",
@@ -758,16 +874,10 @@ Array [
     "type": "minor",
     "unpublishable": false,
   },
-  Object {
-    "newVersion": "1.0.1",
-    "newVersionMajor": 1,
-    "type": "pin",
-    "unpublishable": false,
-  },
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades multiple caret ranges without pinning 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades multiple caret ranges without pinning 1`] = `
 Array [
   Object {
     "fromVersion": "0.7.2",
@@ -792,7 +902,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades multiple tilde ranges without pinning 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades multiple tilde ranges without pinning 1`] = `
 Array [
   Object {
     "fromVersion": "0.7.2",
@@ -817,7 +927,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades shorthand major ranges without pinning 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades shorthand major ranges without pinning 1`] = `
 Array [
   Object {
     "fromVersion": "0.9.7",
@@ -832,7 +942,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades shorthand minor ranges without pinning 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades shorthand minor ranges without pinning 1`] = `
 Array [
   Object {
     "fromVersion": "1.3.0",
@@ -847,8 +957,15 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades tilde ranges 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades tilde ranges 1`] = `
 Array [
+  Object {
+    "isPin": true,
+    "newVersion": "1.3.0",
+    "newVersionMajor": 1,
+    "type": "pin",
+    "unpublishable": false,
+  },
   Object {
     "fromVersion": "1.3.0",
     "newVersion": "1.4.1",
@@ -858,16 +975,10 @@ Array [
     "type": "minor",
     "unpublishable": false,
   },
-  Object {
-    "newVersion": "1.3.0",
-    "newVersionMajor": 1,
-    "type": "pin",
-    "unpublishable": false,
-  },
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) upgrades tilde ranges without pinning 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() upgrades tilde ranges without pinning 1`] = `
 Array [
   Object {
     "fromVersion": "1.3.0",
@@ -882,8 +993,15 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) uses the locked version for pinning 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() uses the locked version for pinning 1`] = `
 Array [
+  Object {
+    "isPin": true,
+    "newVersion": "1.0.0",
+    "newVersionMajor": 1,
+    "type": "pin",
+    "unpublishable": false,
+  },
   Object {
     "fromVersion": "1.0.0",
     "newVersion": "1.4.1",
@@ -893,16 +1011,10 @@ Array [
     "type": "minor",
     "unpublishable": false,
   },
-  Object {
-    "newVersion": "1.0.0",
-    "newVersionMajor": 1,
-    "type": "pin",
-    "unpublishable": false,
-  },
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) widens .x OR ranges 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() widens .x OR ranges 1`] = `
 Array [
   Object {
     "fromVersion": "2.7.0",
@@ -917,7 +1029,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) widens major ranged versions if configured 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() widens major ranged versions if configured 1`] = `
 Array [
   Object {
     "fromVersion": "2.7.0",
@@ -932,7 +1044,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) widens minor ranged versions if configured 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() widens minor ranged versions if configured 1`] = `
 Array [
   Object {
     "fromVersion": "1.3.0",
@@ -947,7 +1059,7 @@ Array [
 ]
 `;
 
-exports[`manager/npm/versions .determineUpgrades(npmDep, config) widens stanndalone major OR ranges 1`] = `
+exports[`manager/npm/lookup .lookupUpdates() widens stanndalone major OR ranges 1`] = `
 Array [
   Object {
     "fromVersion": "2.7.0",
diff --git a/test/manager/npm/versions.spec.js b/test/manager/npm/lookup.spec.js
similarity index 60%
rename from test/manager/npm/versions.spec.js
rename to test/manager/npm/lookup.spec.js
index 5da47fcedff4e50dd59b1bcd42940eea6eaf94bc..19f01519aa6e6e93fd442eb6bec31e67729e23d2 100644
--- a/test/manager/npm/versions.spec.js
+++ b/test/manager/npm/lookup.spec.js
@@ -1,4 +1,4 @@
-const versions = require('../../../lib/manager/npm/versions');
+const lookup = require('../../../lib/manager/npm/lookup');
 const qJson = require('../../_fixtures/npm/01.json');
 const helmetJson = require('../../_fixtures/npm/02.json');
 const coffeelintJson = require('../../_fixtures/npm/coffeelint.json');
@@ -6,60 +6,66 @@ const webpackJson = require('../../_fixtures/npm/webpack.json');
 const nextJson = require('../../_fixtures/npm/next.json');
 const typescriptJson = require('../../_fixtures/npm/typescript.json');
 
+qJson.latestVersion = '1.4.1';
+
 let config;
 
-describe('manager/npm/versions', () => {
+describe('manager/npm/lookup', () => {
   beforeEach(() => {
     config = { ...require('../../../lib/config/defaults').getConfig() };
-    config.rangeStrategy = 'pin';
+    config.rangeStrategy = 'replace';
   });
 
-  describe('.determineUpgrades(npmDep, config)', () => {
-    it('return warning if null versions', () => {
-      config.currentVersion = '1.0.0';
-      const testDep = {
-        name: 'q',
-      };
-      const res = versions.determineUpgrades(testDep, config);
-      expect(res).toHaveLength(1);
-      expect(res[0]).toMatchSnapshot();
-    });
+  describe('.lookupUpdates()', () => {
     it('return warning if empty versions', () => {
       const testDep = {
         name: 'q',
         versions: [],
       };
       config.currentVersion = '1.0.0';
-      const res = versions.determineUpgrades(testDep, config);
+      const res = lookup.lookupUpdates(testDep, config);
       expect(res).toHaveLength(1);
       expect(res[0]).toMatchSnapshot();
     });
-    it('returns warning if range not found', () => {
+    it('returns rollback if range not found', () => {
       config.currentVersion = '^8.4.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('supports minor and major upgrades for tilde ranges', () => {
       config.currentVersion = '^0.4.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      config.rangeStrategy = 'pin';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('returns only one update if grouping', () => {
       config.groupName = 'somegroup';
       config.currentVersion = '^0.4.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      config.rangeStrategy = 'pin';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
+    });
+    it('returns only one update if automerging', () => {
+      config.automerge = true;
+      config.currentVersion = '0.4.0';
+      config.rangeStrategy = 'pin';
+      const res = lookup.lookupUpdates(qJson, config);
+      expect(res).toMatchSnapshot();
+      expect(res).toHaveLength(1);
     });
     it('returns only one update if automerging major', () => {
       config.major = { automerge: true };
       config.currentVersion = '^0.4.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      config.rangeStrategy = 'pin';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('returns both updates if automerging minor', () => {
       config.minor = { automerge: true };
       config.currentVersion = '^0.4.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      config.rangeStrategy = 'pin';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('returns minor update if separate patches not configured', () => {
       config.currentVersion = '0.9.0';
-      const res = versions.determineUpgrades(qJson, config);
+      config.rangeStrategy = 'pin';
+      const res = lookup.lookupUpdates(qJson, config);
       expect(res).toMatchSnapshot();
       expect(res.length).toBe(2);
       expect(res[0].type).not.toEqual('patch');
@@ -70,7 +76,8 @@ describe('manager/npm/versions', () => {
         automerge: true,
       };
       config.currentVersion = '0.9.0';
-      const res = versions.determineUpgrades(qJson, config);
+      config.rangeStrategy = 'pin';
+      const res = lookup.lookupUpdates(qJson, config);
       expect(res).toMatchSnapshot();
       expect(res[0].type).toEqual('patch');
     });
@@ -82,229 +89,258 @@ describe('manager/npm/versions', () => {
         automerge: true,
       };
       config.currentVersion = '0.9.0';
-      const res = versions.determineUpgrades(qJson, config);
+      config.rangeStrategy = 'pin';
+      const res = lookup.lookupUpdates(qJson, config);
       expect(res).toMatchSnapshot();
       expect(res[0].type).toEqual('minor');
     });
     it('returns patch update if separateMinorPatch', () => {
       config.separateMinorPatch = true;
       config.currentVersion = '0.9.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      config.rangeStrategy = 'pin';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('returns patch minor and major', () => {
       config.separateMinorPatch = true;
       config.currentVersion = '0.8.0';
-      const res = versions.determineUpgrades(qJson, config);
+      config.rangeStrategy = 'pin';
+      const res = lookup.lookupUpdates(qJson, config);
       expect(res).toHaveLength(3);
       expect(res).toMatchSnapshot();
     });
     it('disables major release separation (major)', () => {
       config.separateMajorMinor = false;
       config.currentVersion = '^0.4.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      config.rangeStrategy = 'pin';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('disables major release separation (minor)', () => {
       config.separateMajorMinor = false;
       config.currentVersion = '1.0.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      config.rangeStrategy = 'pin';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('supports minor and major upgrades for ranged versions', () => {
       config.currentVersion = '~0.4.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      config.rangeStrategy = 'pin';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('ignores pinning for ranges when other upgrade exists', () => {
       config.currentVersion = '~0.9.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      config.rangeStrategy = 'pin';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('upgrades minor ranged versions', () => {
       config.currentVersion = '~1.0.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      config.rangeStrategy = 'pin';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('widens minor ranged versions if configured', () => {
       config.currentVersion = '~1.3.0';
       config.rangeStrategy = 'widen';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('replaces minor complex ranged versions if configured', () => {
       config.currentVersion = '~1.2.0 || ~1.3.0';
       config.rangeStrategy = 'replace';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('widens major ranged versions if configured', () => {
       config.currentVersion = '^2.0.0';
       config.rangeStrategy = 'widen';
-      expect(versions.determineUpgrades(webpackJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(webpackJson, config)).toMatchSnapshot();
     });
     it('replaces major complex ranged versions if configured', () => {
       config.currentVersion = '^1.0.0 || ^2.0.0';
       config.rangeStrategy = 'replace';
-      expect(versions.determineUpgrades(webpackJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(webpackJson, config)).toMatchSnapshot();
     });
     it('pins minor ranged versions', () => {
       config.currentVersion = '^1.0.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      config.rangeStrategy = 'pin';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('uses the locked version for pinning', () => {
       config.currentVersion = '^1.0.0';
       config.lockedVersion = '1.0.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      config.rangeStrategy = 'pin';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('ignores minor ranged versions when not pinning', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '^1.0.0';
-      expect(versions.determineUpgrades(qJson, config)).toHaveLength(0);
+      expect(lookup.lookupUpdates(qJson, config)).toHaveLength(0);
     });
     it('upgrades tilde ranges', () => {
+      config.rangeStrategy = 'pin';
       config.currentVersion = '~1.3.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('upgrades .x minor ranges', () => {
       config.currentVersion = '1.3.x';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      config.rangeStrategy = 'pin';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('upgrades tilde ranges without pinning', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '~1.3.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('upgrades .x major ranges without pinning', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '0.x';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('upgrades .x minor ranges without pinning', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '1.3.x';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
+    });
+    it('upgrades .x complex minor ranges without pinning', () => {
+      config.rangeStrategy = 'widen';
+      config.currentVersion = '1.2.x - 1.3.x';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('upgrades shorthand major ranges without pinning', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('upgrades shorthand minor ranges without pinning', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '1.3';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('upgrades multiple tilde ranges without pinning', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '~0.7.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('upgrades multiple caret ranges without pinning', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '^0.7.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('supports complex ranges', () => {
       config.rangeStrategy = 'widen';
       config.currentVersion = '^0.7.0 || ^0.8.0';
-      const res = versions.determineUpgrades(qJson, config);
+      const res = lookup.lookupUpdates(qJson, config);
       expect(res).toHaveLength(2);
       expect(res[0]).toMatchSnapshot();
     });
     it('supports complex major ranges', () => {
       config.rangeStrategy = 'widen';
       config.currentVersion = '^1.0.0 || ^2.0.0';
-      const res = versions.determineUpgrades(webpackJson, config);
+      const res = lookup.lookupUpdates(webpackJson, config);
       expect(res).toMatchSnapshot();
     });
     it('supports complex major hyphen ranges', () => {
       config.rangeStrategy = 'widen';
       config.currentVersion = '1.x - 2.x';
-      const res = versions.determineUpgrades(webpackJson, config);
+      const res = lookup.lookupUpdates(webpackJson, config);
       expect(res).toMatchSnapshot();
     });
     it('widens .x OR ranges', () => {
       config.rangeStrategy = 'widen';
       config.currentVersion = '1.x || 2.x';
-      const res = versions.determineUpgrades(webpackJson, config);
+      const res = lookup.lookupUpdates(webpackJson, config);
       expect(res).toMatchSnapshot();
     });
     it('widens stanndalone major OR ranges', () => {
       config.rangeStrategy = 'widen';
       config.currentVersion = '1 || 2';
-      const res = versions.determineUpgrades(webpackJson, config);
+      const res = lookup.lookupUpdates(webpackJson, config);
       expect(res).toMatchSnapshot();
     });
     it('supports complex tilde ranges', () => {
       config.rangeStrategy = 'widen';
       config.currentVersion = '~1.2.0 || ~1.3.0';
-      const res = versions.determineUpgrades(qJson, config);
+      const res = lookup.lookupUpdates(qJson, config);
       expect(res).toMatchSnapshot();
     });
     it('returns nothing for greater than ranges', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '>= 0.7.0';
-      expect(versions.determineUpgrades(qJson, config)).toHaveLength(0);
+      expect(lookup.lookupUpdates(qJson, config)).toHaveLength(0);
     });
     it('upgrades less than equal ranges without pinning', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '<= 0.7.2';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('upgrades less than ranges without pinning', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '< 0.7.2';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
+    });
+    it('upgrades less than major ranges', () => {
+      config.rangeStrategy = 'replace';
+      config.currentVersion = '< 1';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
+    });
+    it('upgrades less than equal minor ranges', () => {
+      config.rangeStrategy = 'replace';
+      config.currentVersion = '<= 1.3';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
+    });
+    it('upgrades less than equal major ranges', () => {
+      config.rangeStrategy = 'replace';
+      config.respectLatest = false;
+      config.currentVersion = '<= 1';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('upgrades major less than equal ranges', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '<= 1.0.0';
-      const res = versions.determineUpgrades(qJson, config);
+      const res = lookup.lookupUpdates(qJson, config);
       expect(res).toMatchSnapshot();
-      expect(res[0].newVersion).toEqual('<= 2.0.0');
+      expect(res[0].newVersion).toEqual('<= 1.4.1');
     });
     it('upgrades major less than ranges without pinning', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '< 1.0.0';
-      const res = versions.determineUpgrades(qJson, config);
+      const res = lookup.lookupUpdates(qJson, config);
       expect(res).toMatchSnapshot();
       expect(res[0].newVersion).toEqual('< 2.0.0');
     });
     it('upgrades major greater than less than ranges without pinning', () => {
-      config.rangeStrategy = 'replace';
+      config.rangeStrategy = 'widen';
       config.currentVersion = '>= 0.5.0 < 1.0.0';
-      const res = versions.determineUpgrades(qJson, config);
+      const res = lookup.lookupUpdates(qJson, config);
       expect(res).toMatchSnapshot();
       expect(res[0].newVersion).toEqual('>= 0.5.0 < 2.0.0');
     });
     it('upgrades minor greater than less than ranges without pinning', () => {
-      config.rangeStrategy = 'replace';
+      config.rangeStrategy = 'widen';
       config.currentVersion = '>= 0.5.0 <0.8';
-      const res = versions.determineUpgrades(qJson, config);
+      const res = lookup.lookupUpdates(qJson, config);
       expect(res).toMatchSnapshot();
       expect(res[0].newVersion).toEqual('>= 0.5.0 <0.10');
       expect(res[1].newVersion).toEqual('>= 0.5.0 <1.5');
     });
     it('upgrades minor greater than less than equals ranges without pinning', () => {
-      config.rangeStrategy = 'replace';
+      config.rangeStrategy = 'widen';
       config.currentVersion = '>= 0.5.0 <= 0.8.0';
-      const res = versions.determineUpgrades(qJson, config);
+      const res = lookup.lookupUpdates(qJson, config);
       expect(res).toMatchSnapshot();
-      expect(res[0].newVersion).toEqual('>= 0.5.0 <= 0.10.0');
-      expect(res[1].newVersion).toEqual('>= 0.5.0 <= 1.5.0');
+      expect(res[0].newVersion).toEqual('>= 0.5.0 <= 0.9.7');
+      expect(res[1].newVersion).toEqual('>= 0.5.0 <= 1.4.1');
     });
     it('rejects reverse ordered less than greater than', () => {
-      config.rangeStrategy = 'replace';
+      config.rangeStrategy = 'widen';
       config.currentVersion = '<= 0.8.0 >= 0.5.0';
-      const res = versions.determineUpgrades(qJson, config);
+      const res = lookup.lookupUpdates(qJson, config);
       expect(res).toMatchSnapshot();
     });
     it('supports > latest versions if configured', () => {
       config.respectLatest = false;
       config.currentVersion = '1.4.1';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
-    });
-    it('supports future versions if already future', () => {
-      config.currentVersion = '^2.0.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('should ignore unstable versions if the current version is stable', () => {
       config.currentVersion = '1.0.0';
-      versions
-        .determineUpgrades(
+      lookup
+        .lookupUpdates(
           {
             name: 'amazing-package',
             versions: {
@@ -316,10 +352,43 @@ describe('manager/npm/versions', () => {
         )
         .should.eql([]);
     });
+    it('should allow unstable versions if the ignoreUnstable=false', () => {
+      config.currentVersion = '1.0.0';
+      config.ignoreUnstable = false;
+      expect(
+        lookup.lookupUpdates(
+          {
+            name: 'amazing-package',
+            versions: {
+              '1.0.0-beta': {},
+              '1.0.0': {},
+              '1.1.0-beta': {},
+            },
+          },
+          config
+        )
+      ).toMatchSnapshot();
+    });
     it('should allow unstable versions if the current version is unstable', () => {
       config.currentVersion = '1.0.0-beta';
       expect(
-        versions.determineUpgrades(
+        lookup.lookupUpdates(
+          {
+            name: 'amazing-package',
+            versions: {
+              '1.0.0-beta': {},
+              '1.1.0-beta': {},
+            },
+          },
+          config
+        )
+      ).toMatchSnapshot();
+    });
+    it('should allow unstable versions if ignoreUnstable is false', () => {
+      config.currentVersion = '1.0.0';
+      config.ignoreUnstable = false;
+      expect(
+        lookup.lookupUpdates(
           {
             name: 'amazing-package',
             versions: {
@@ -334,77 +403,73 @@ describe('manager/npm/versions', () => {
     it('should treat zero zero tilde ranges as 0.0.x', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '~0.0.34';
-      expect(versions.determineUpgrades(helmetJson, config)).toEqual([]);
+      expect(lookup.lookupUpdates(helmetJson, config)).toEqual([]);
     });
     it('should treat zero zero caret ranges as pinned', () => {
       config.rangeStrategy = 'replace';
       config.currentVersion = '^0.0.34';
-      expect(versions.determineUpgrades(helmetJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(helmetJson, config)).toMatchSnapshot();
     });
     it('should downgrade from missing versions', () => {
       config.currentVersion = '1.16.1';
-      const res = versions.determineUpgrades(coffeelintJson, config);
+      const res = lookup.lookupUpdates(coffeelintJson, config);
       expect(res).toHaveLength(1);
       expect(res[0]).toMatchSnapshot();
     });
     it('should upgrade to only one major', () => {
       config.currentVersion = '1.0.0';
-      const res = versions.determineUpgrades(webpackJson, config);
+      const res = lookup.lookupUpdates(webpackJson, config);
       expect(res).toHaveLength(2);
     });
     it('should upgrade to two majors', () => {
       config.currentVersion = '1.0.0';
       config.separateMultipleMajor = true;
-      const res = versions.determineUpgrades(webpackJson, config);
+      const res = lookup.lookupUpdates(webpackJson, config);
       expect(res).toHaveLength(3);
     });
     it('does not jump  major unstable', () => {
       config.currentVersion = '^4.4.0-canary.3';
       config.rangeStrategy = 'replace';
-      const res = versions.determineUpgrades(nextJson, config);
+      const res = lookup.lookupUpdates(nextJson, config);
       expect(res).toHaveLength(0);
     });
     it('handles prerelease jumps', () => {
       config.currentVersion = '^2.9.0-rc';
       config.rangeStrategy = 'replace';
-      const res = versions.determineUpgrades(typescriptJson, config);
+      const res = lookup.lookupUpdates(typescriptJson, config);
       expect(res).toMatchSnapshot();
     });
-    it('supports in-range updates', () => {
+    it('supports in-range caret updates', () => {
+      config.rangeStrategy = 'bump';
+      config.currentVersion = '^1.0.0';
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
+    });
+    it('supports in-range tilde updates', () => {
       config.rangeStrategy = 'bump';
       config.currentVersion = '~1.0.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('rejects in-range unsupported operator', () => {
       config.rangeStrategy = 'bump';
       config.currentVersion = '>=1.0.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('rejects non-fully specified in-range updates', () => {
       config.rangeStrategy = 'bump';
       config.currentVersion = '1.x';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('rejects complex range in-range updates', () => {
       config.rangeStrategy = 'bump';
       config.currentVersion = '^0.9.0 || ^1.0.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
     it('rejects non-range in-range updates', () => {
+      config.depName = 'q';
+      config.packageFile = 'package.json';
       config.rangeStrategy = 'bump';
       config.currentVersion = '1.0.0';
-      expect(versions.determineUpgrades(qJson, config)).toMatchSnapshot();
-    });
-  });
-  describe('.isPastLatest(dep, version)', () => {
-    it('should return false for less than', () => {
-      versions.isPastLatest(qJson, '1.0.0').should.eql(false);
-    });
-    it('should return false for equal', () => {
-      versions.isPastLatest(qJson, '1.4.1').should.eql(false);
-    });
-    it('should return true for greater than', () => {
-      versions.isPastLatest(qJson, '2.0.3').should.eql(true);
+      expect(lookup.lookupUpdates(qJson, config)).toMatchSnapshot();
     });
   });
 });
diff --git a/test/manager/npm/package.spec.js b/test/manager/npm/package.spec.js
index 80241a5b685e6a4c0282c5deb359c30df97fc5da..b3b660a284cfbf9b7b562ec5272e3c24ffa361d0 100644
--- a/test/manager/npm/package.spec.js
+++ b/test/manager/npm/package.spec.js
@@ -1,5 +1,5 @@
 const npmApi = require('../../../lib/datasource/npm');
-const versions = require('../../../lib/manager/npm/versions');
+const lookup = require('../../../lib/manager/npm/lookup');
 const npm = require('../../../lib/manager/npm/package');
 const defaultConfig = require('../../../lib/config/defaults').getConfig();
 
@@ -84,7 +84,7 @@ describe('lib/manager/npm/package', () => {
     });
     it('returns warning if warning found', async () => {
       npmApi.getDependency.mockReturnValueOnce({});
-      versions.determineUpgrades = jest.fn(() => [
+      lookup.lookupUpdates = jest.fn(() => [
         {
           type: 'warning',
           message: 'bad version',
@@ -95,7 +95,7 @@ describe('lib/manager/npm/package', () => {
     });
     it('returns array if upgrades found', async () => {
       npmApi.getDependency.mockReturnValueOnce({ repositoryUrl: 'some-url' });
-      versions.determineUpgrades = jest.fn(() => [{}]);
+      lookup.lookupUpdates = jest.fn(() => [{}]);
       const res = await npm.getPackageUpdates(config);
       expect(res).toHaveLength(1);
       expect(Object.keys(res[0])).toMatchSnapshot();
@@ -104,7 +104,7 @@ describe('lib/manager/npm/package', () => {
     it('sets repositoryUrl for @types', async () => {
       config.depName = '@types/some-dep';
       npmApi.getDependency.mockReturnValueOnce({});
-      versions.determineUpgrades = jest.fn(() => [{}]);
+      lookup.lookupUpdates = jest.fn(() => [{}]);
       const res = await npm.getPackageUpdates(config);
       expect(res[0].repositoryUrl).toEqual(
         'https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/some-dep'
diff --git a/test/util/semver.spec.js b/test/util/semver.spec.js
index 814b0cb23cd6406965e910302f2b5a44df0ab28c..31aa5babc45a9d193d7909bac1ecb72500f89930 100644
--- a/test/util/semver.spec.js
+++ b/test/util/semver.spec.js
@@ -1,28 +1,28 @@
 const semver = require('../../lib/versioning/semver');
 
-describe('.isValidSemver(input)', () => {
+describe('.isValid(input)', () => {
   it('should return null for irregular versions', () => {
-    expect(!!semver.isValidSemver('17.04.0')).toBe(false);
+    expect(!!semver.isValid('17.04.0')).toBe(false);
   });
   it('should support simple semver', () => {
-    expect(!!semver.isValidSemver('1.2.3')).toBe(true);
+    expect(!!semver.isValid('1.2.3')).toBe(true);
   });
   it('should support semver with dash', () => {
-    expect(!!semver.isValidSemver('1.2.3-foo')).toBe(true);
+    expect(!!semver.isValid('1.2.3-foo')).toBe(true);
   });
   it('should reject semver without dash', () => {
-    expect(!!semver.isValidSemver('1.2.3foo')).toBe(false);
+    expect(!!semver.isValid('1.2.3foo')).toBe(false);
   });
   it('should support ranges', () => {
-    expect(!!semver.isValidSemver('~1.2.3')).toBe(true);
-    expect(!!semver.isValidSemver('^1.2.3')).toBe(true);
-    expect(!!semver.isValidSemver('>1.2.3')).toBe(true);
+    expect(!!semver.isValid('~1.2.3')).toBe(true);
+    expect(!!semver.isValid('^1.2.3')).toBe(true);
+    expect(!!semver.isValid('>1.2.3')).toBe(true);
   });
   it('should reject github repositories', () => {
-    expect(!!semver.isValidSemver('renovateapp/renovate')).toBe(false);
-    expect(!!semver.isValidSemver('renovateapp/renovate#master')).toBe(false);
+    expect(!!semver.isValid('renovateapp/renovate')).toBe(false);
+    expect(!!semver.isValid('renovateapp/renovate#master')).toBe(false);
     expect(
-      !!semver.isValidSemver('https://github.com/renovateapp/renovate.git')
+      !!semver.isValid('https://github.com/renovateapp/renovate.git')
     ).toBe(false);
   });
 });