@voxpelli/semver-set
Version:
Finds intersections between semantic version ranges.
204 lines (163 loc) • 6.12 kB
JavaScript
import Comparator from 'semver/classes/comparator.js';
import compare from 'semver/functions/compare.js';
import satisfies from 'semver/functions/satisfies.js';
// TODO: Add this to @types/semver
// @ts-ignore
const ANY = Comparator.ANY;
// Somewhat based on https://github.com/npm/node-semver/blob/e08d9167937e09e8e6fe23aacaf17f892a1d69e1/ranges/subset.js
// >=1.2.3 is lower than >1.2.3
/**
* @param {import('semver').Comparator|undefined} a
* @param {import('semver').Comparator} b
* @param {import('semver').Options} options
* @returns {import('semver').Comparator}
*/
const higherGT = (a, b, options) => {
if (!a) return b;
const comp = compare(a.semver, b.semver, options);
if (comp > 0) return a;
if (comp < 0) return b;
if (b.operator === '>' && a.operator === '>=') return b;
return a;
};
// <=1.2.3 is higher than <1.2.3
/**
* @param {import('semver').Comparator|undefined} a
* @param {import('semver').Comparator} b
* @param {import('semver').Options} options
* @returns {import('semver').Comparator}
*/
const lowerLT = (a, b, options) => {
if (!a) return b;
const comp = compare(a.semver, b.semver, options);
if (comp < 0) return a;
if (comp > 0) return b;
if (b.operator === '<' && a.operator === '<=') return b;
return a;
};
/** @typedef {[import('semver').Comparator]} CompactedComparatorFixed */
/** @typedef {[import('semver').Comparator|undefined,import('semver').Comparator|undefined]} CompactedComparatorRange */
/** @typedef {CompactedComparatorFixed|CompactedComparatorRange} CompactedComparator */
/**
* @param {ReadonlyArray<import('semver').Comparator|undefined>} comparators
* @param {import('semver').Options} options
* @returns {CompactedComparator|undefined}
*/
const compactComparators = (comparators, options) => {
/** @type {import('semver').Comparator|undefined} */
let gt;
/** @type {import('semver').Comparator|undefined} */
let lt;
/** @type {Set<import('semver').SemVer>} */
const eqSet = new Set();
for (const c of comparators) {
if (c === undefined) {
continue;
} else if (c.operator === '>' || c.operator === '>=') {
gt = higherGT(gt, c, options);
} else if (c.operator === '<' || c.operator === '<=') {
lt = lowerLT(lt, c, options);
} else if (c.semver !== ANY) {
eqSet.add(c.semver);
}
}
// As every value in a Set can only exists once: If we have multiple values in there, then we expect an AND relation between multiple fixed versions. That can never be fulfilled.
if (eqSet.size > 1) return;
/** @type {0|1|-1|undefined} */
let gtltComp;
// We can't have an AND relation between two ranges if they don't overlap
if (gt && lt) {
gtltComp = compare(gt.semver, lt.semver, options);
if (gtltComp > 0) return;
if (gtltComp === 0 && (gt.operator !== '>=' || lt.operator !== '<=')) return;
}
const eq = [...eqSet].shift();
// If we only have ranges, ignoring th return those
if (eq === undefined) return [gt, lt];
// We can't have an AND relation between a range and a value if the two doesn't overlap
if (gt && !satisfies(eq, String(gt), options)) return;
if (lt && !satisfies(eq, String(lt), options)) return;
// The equality value always wins when its there. And if no value is there: Then we opt for ANY
return [new Comparator(String(eq || ''))];
};
/**
* @param {ReadonlyArray<import('semver').Comparator|undefined>} compA
* @param {ReadonlyArray<import('semver').Comparator|undefined>} compB
* @param {import('semver').Options} options
* @returns {CompactedComparator|false|undefined}
*/
const calculateSubset = (compA, compB, options) => {
if (
compA.length === 1 && compA[0] && compA[0].semver === ANY &&
compB.length === 1 && compB[0] && compB[0].semver === ANY
) {
return [compA[0]];
}
const compactedA = compactComparators(compA, options);
const compactedB = compactComparators(compB, options);
if (!compactedA || !compactedB) return;
return compactComparators([...compactedA, ...compactedB], options) || false;
};
/**
* @param {import('semver').Range} sub
* @param {import('semver').Range} dom
* @param {import('semver').Options} options
* @returns {CompactedComparator[]|undefined}
*/
export function rangeIntersection (sub, dom, options) {
/** @type {Array<CompactedComparator|undefined>|undefined} */
let subsets;
for (const simpleSub of sub.set) {
for (const simpleDom of dom.set) {
const result = calculateSubset(simpleSub, simpleDom, options);
subsets = subsets || [];
if (!result) continue;
/** @type {CompactedComparator|undefined} */
let modifiedCompactedComparator;
for (let i = 0, length = subsets.length; i < length; i++) {
const subset = subsets[i];
if (!subset) continue;
const subsetWithSubset = compactComparators([...subset, ...result], options);
if (!subsetWithSubset) continue;
if (modifiedCompactedComparator) {
subsets[i] = undefined;
continue;
}
if (subsetWithSubset.length === 1 && result.length === 2) {
subsets[i] = result;
} else {
if (subset[0] && (result[0] === undefined || higherGT(subset[0], result[0], options) === subset[0])) {
subset[0] = result[0];
}
if (subset[1] && (result[1] === undefined || lowerLT(subset[1], result[1], options) === subset[1])) {
subset[1] = result[1];
}
}
modifiedCompactedComparator = subsets[i];
}
if (!modifiedCompactedComparator) {
subsets.push(result);
}
}
}
if (!subsets) return;
// @ts-ignore
return subsets.filter(item => !!item);
}
/**
* @param {CompactedComparator[]} intersection
*/
export function sortRangeIntersection (intersection) {
intersection.sort((a, b) => {
if (a[0]) {
if (!b[0]) return -1;
return compare(a[0].semver, b[0].semver);
}
if (a[1]) {
if (!b[1]) return -1;
return compare(a[1].semver, b[1].semver);
}
if (b[0] || b[1]) return 1;
return 0;
});
}