@naturalcycles/js-lib
Version:
Standard library for universal (browser + Node.js) javascript
151 lines (150 loc) • 4.3 kB
JavaScript
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;
}