/** * Copyright (c) 2013-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @providesModule VersionRange */ 'use strict'; const invariant = require('./invariant'); const componentRegex = /\./; const orRegex = /\|\|/; const rangeRegex = /\s+\-\s+/; const modifierRegex = /^(<=|<|=|>=|~>|~|>|)?\s*(.+)/; const numericRegex = /^(\d*)(.*)/; /** * Splits input `range` on "||" and returns true if any subrange matches * `version`. * * @param {string} range * @param {string} version * @returns {boolean} */ function checkOrExpression(range, version) { const expressions = range.split(orRegex); if (expressions.length > 1) { return expressions.some(range => VersionRange.contains(range, version)); } else { range = expressions[0].trim(); return checkRangeExpression(range, version); } } /** * Splits input `range` on " - " (the surrounding whitespace is required) and * returns true if version falls between the two operands. * * @param {string} range * @param {string} version * @returns {boolean} */ function checkRangeExpression(range, version) { const expressions = range.split(rangeRegex); invariant(expressions.length > 0 && expressions.length <= 2, 'the "-" operator expects exactly 2 operands'); if (expressions.length === 1) { return checkSimpleExpression(expressions[0], version); } else { const [startVersion, endVersion] = expressions; invariant(isSimpleVersion(startVersion) && isSimpleVersion(endVersion), 'operands to the "-" operator must be simple (no modifiers)'); return checkSimpleExpression('>=' + startVersion, version) && checkSimpleExpression('<=' + endVersion, version); } } /** * Checks if `range` matches `version`. `range` should be a "simple" range (ie. * not a compound range using the " - " or "||" operators). * * @param {string} range * @param {string} version * @returns {boolean} */ function checkSimpleExpression(range, version) { range = range.trim(); if (range === '') { return true; } const versionComponents = version.split(componentRegex); const { modifier, rangeComponents } = getModifierAndComponents(range); switch (modifier) { case '<': return checkLessThan(versionComponents, rangeComponents); case '<=': return checkLessThanOrEqual(versionComponents, rangeComponents); case '>=': return checkGreaterThanOrEqual(versionComponents, rangeComponents); case '>': return checkGreaterThan(versionComponents, rangeComponents); case '~': case '~>': return checkApproximateVersion(versionComponents, rangeComponents); default: return checkEqual(versionComponents, rangeComponents); } } /** * Checks whether `a` is less than `b`. * * @param {array} a * @param {array} b * @returns {boolean} */ function checkLessThan(a, b) { return compareComponents(a, b) === -1; } /** * Checks whether `a` is less than or equal to `b`. * * @param {array} a * @param {array} b * @returns {boolean} */ function checkLessThanOrEqual(a, b) { const result = compareComponents(a, b); return result === -1 || result === 0; } /** * Checks whether `a` is equal to `b`. * * @param {array} a * @param {array} b * @returns {boolean} */ function checkEqual(a, b) { return compareComponents(a, b) === 0; } /** * Checks whether `a` is greater than or equal to `b`. * * @param {array} a * @param {array} b * @returns {boolean} */ function checkGreaterThanOrEqual(a, b) { const result = compareComponents(a, b); return result === 1 || result === 0; } /** * Checks whether `a` is greater than `b`. * * @param {array} a * @param {array} b * @returns {boolean} */ function checkGreaterThan(a, b) { return compareComponents(a, b) === 1; } /** * Checks whether `a` is "reasonably close" to `b` (as described in * https://www.npmjs.org/doc/misc/semver.html). For example, if `b` is "1.3.1" * then "reasonably close" is defined as ">= 1.3.1 and < 1.4". * * @param {array} a * @param {array} b * @returns {boolean} */ function checkApproximateVersion(a, b) { const lowerBound = b.slice(); const upperBound = b.slice(); if (upperBound.length > 1) { upperBound.pop(); } const lastIndex = upperBound.length - 1; const numeric = parseInt(upperBound[lastIndex], 10); if (isNumber(numeric)) { upperBound[lastIndex] = numeric + 1 + ''; } return checkGreaterThanOrEqual(a, lowerBound) && checkLessThan(a, upperBound); } /** * Extracts the optional modifier (<, <=, =, >=, >, ~, ~>) and version * components from `range`. * * For example, given `range` ">= 1.2.3" returns an object with a `modifier` of * `">="` and `components` of `[1, 2, 3]`. * * @param {string} range * @returns {object} */ function getModifierAndComponents(range) { const rangeComponents = range.split(componentRegex); const matches = rangeComponents[0].match(modifierRegex); invariant(matches, 'expected regex to match but it did not'); return { modifier: matches[1], rangeComponents: [matches[2]].concat(rangeComponents.slice(1)) }; } /** * Determines if `number` is a number. * * @param {mixed} number * @returns {boolean} */ function isNumber(number) { return !isNaN(number) && isFinite(number); } /** * Tests whether `range` is a "simple" version number without any modifiers * (">", "~" etc). * * @param {string} range * @returns {boolean} */ function isSimpleVersion(range) { return !getModifierAndComponents(range).modifier; } /** * Zero-pads array `array` until it is at least `length` long. * * @param {array} array * @param {number} length */ function zeroPad(array, length) { for (let i = array.length; i < length; i++) { array[i] = '0'; } } /** * Normalizes `a` and `b` in preparation for comparison by doing the following: * * - zero-pads `a` and `b` * - marks any "x", "X" or "*" component in `b` as equivalent by zero-ing it out * in both `a` and `b` * - marks any final "*" component in `b` as a greedy wildcard by zero-ing it * and all of its successors in `a` * * @param {array} a * @param {array} b * @returns {array>} */ function normalizeVersions(a, b) { a = a.slice(); b = b.slice(); zeroPad(a, b.length); // mark "x" and "*" components as equal for (let i = 0; i < b.length; i++) { const matches = b[i].match(/^[x*]$/i); if (matches) { b[i] = a[i] = '0'; // final "*" greedily zeros all remaining components if (matches[0] === '*' && i === b.length - 1) { for (let j = i; j < a.length; j++) { a[j] = '0'; } } } } zeroPad(b, a.length); return [a, b]; } /** * Returns the numerical -- not the lexicographical -- ordering of `a` and `b`. * * For example, `10-alpha` is greater than `2-beta`. * * @param {string} a * @param {string} b * @returns {number} -1, 0 or 1 to indicate whether `a` is less than, equal to, * or greater than `b`, respectively */ function compareNumeric(a, b) { const aPrefix = a.match(numericRegex)[1]; const bPrefix = b.match(numericRegex)[1]; const aNumeric = parseInt(aPrefix, 10); const bNumeric = parseInt(bPrefix, 10); if (isNumber(aNumeric) && isNumber(bNumeric) && aNumeric !== bNumeric) { return compare(aNumeric, bNumeric); } else { return compare(a, b); } } /** * Returns the ordering of `a` and `b`. * * @param {string|number} a * @param {string|number} b * @returns {number} -1, 0 or 1 to indicate whether `a` is less than, equal to, * or greater than `b`, respectively */ function compare(a, b) { invariant(typeof a === typeof b, '"a" and "b" must be of the same type'); if (a > b) { return 1; } else if (a < b) { return -1; } else { return 0; } } /** * Compares arrays of version components. * * @param {array} a * @param {array} b * @returns {number} -1, 0 or 1 to indicate whether `a` is less than, equal to, * or greater than `b`, respectively */ function compareComponents(a, b) { const [aNormalized, bNormalized] = normalizeVersions(a, b); for (let i = 0; i < bNormalized.length; i++) { const result = compareNumeric(aNormalized[i], bNormalized[i]); if (result) { return result; } } return 0; } var VersionRange = { /** * Checks whether `version` satisfies the `range` specification. * * We support a subset of the expressions defined in * https://www.npmjs.org/doc/misc/semver.html: * * version Must match version exactly * =version Same as just version * >version Must be greater than version * >=version Must be greater than or equal to version * = 1.2.3 and < 1.3" * ~>version Equivalent to ~version * 1.2.x Must match "1.2.x", where "x" is a wildcard that matches * anything * 1.2.* Similar to "1.2.x", but "*" in the trailing position is a * "greedy" wildcard, so will match any number of additional * components: * "1.2.*" will match "1.2.1", "1.2.1.1", "1.2.1.1.1" etc * * Any version * "" (Empty string) Same as * * v1 - v2 Equivalent to ">= v1 and <= v2" * r1 || r2 Passes if either r1 or r2 are satisfied * * @param {string} range * @param {string} version * @returns {boolean} */ contains(range, version) { return checkOrExpression(range.trim(), version.trim()); } }; module.exports = VersionRange;