UNPKG

@naturalcycles/js-lib

Version:

Standard library for universal (browser + Node.js) javascript

151 lines (150 loc) 4.3 kB
import { _range } from './array/range.js'; import { _assert } from './error/assert.js'; /** * Simple Semver implementation. * * Suitable for Browser usage, unlike npm `semver` which is Node-targeted and simply too big for the Browser. * * Parsing algorithm is simple: * 1. Split by `.` * 2. parseInt each of 3 tokens, set to 0 if falsy * * toString returns `major.minor.patch` * Missing tokens are replaced with 0. * * _semver('1').toString() === '1.0.0' * * @experimental */ export class Semver { tokens; constructor(tokens) { this.tokens = tokens; } get major() { return this.tokens[0]; } get minor() { return this.tokens[1]; } get patch() { return this.tokens[2]; } isAfter = (other) => this.compare(other) > 0; isSameOrAfter = (other) => this.compare(other) >= 0; isBefore = (other) => this.compare(other) < 0; isSameOrBefore = (other) => this.compare(other) <= 0; isSame = (other) => this.compare(other) === 0; /** * Returns 1 if this > other * returns 0 if they are equal * returns -1 if this < other */ compare(other) { const { tokens } = semver2.fromInput(other); for (let i = 0; i < 3; i++) { if (this.tokens[i] < tokens[i]) return -1; if (this.tokens[i] > tokens[i]) return 1; } return 0; } toJSON = () => this.toString(); toString() { return this.tokens.join('.'); } } class SemverFactory { fromInput(input) { const s = this.fromInputOrUndefined(input); _assert(s, `Cannot parse "${input}" into Semver`, { input, }); return s; } fromInputOrUndefined(input) { if (!input) return; if (input instanceof Semver) return input; const t = input.split('.'); return new Semver(_range(3).map(i => parseInt(t[i]) || 0)); } /** * Returns the highest (max) Semver from the array, or undefined if the array is empty. */ maxOrUndefined(items) { let max; for (const item of items) { const input = this.fromInputOrUndefined(item); if (!max || input?.isAfter(max)) { max = input; } } return max; } /** * Returns the highest Semver from the array. * Throws if the array is empty. */ max(items) { const max = this.maxOrUndefined(items); _assert(max, 'semver.max called on empty array'); return max; } /** * Returns the lowest (min) Semver from the array, or undefined if the array is empty. */ minOrUndefined(items) { let min; for (const item of items) { const input = this.fromInputOrUndefined(item); if (!min || input?.isBefore(min)) { min = input; } } return min; } /** * Returns the lowest Semver from the array. * Throws if the array is empty. */ min(items) { const min = this.minOrUndefined(items); _assert(min, 'semver.min called on empty array'); return min; } /** * Sorts an array of Semvers in `dir` order (ascending by default). */ sort(items, opt = {}) { const mod = opt.dir === 'desc' ? -1 : 1; return (opt.mutate ? items : [...items]).sort((a, b) => a.compare(b) * mod); } } const semverFactory = new SemverFactory(); export const semver2 = semverFactory.fromInput.bind(semverFactory); // The line below is the blackest of black magic I have ever written in 2024. // And probably 2023 as well. Object.setPrototypeOf(semver2, semverFactory); /** * Returns 1 if a > b * returns 0 if they are equal * returns -1 if a < b * * Quick&dirty implementation, which should suffice for 95% of the cases. * * Credit: https://stackoverflow.com/a/47159772/4919972 */ export function _quickSemverCompare(a, b) { const t1 = a.split('.'); const t2 = b.split('.'); const s1 = _range(3) .map(i => (t1[i] || '').padStart(5)) .join(''); const s2 = _range(3) .map(i => (t2[i] || '').padStart(5)) .join(''); return s1 < s2 ? -1 : s1 > s2 ? 1 : 0; }