1038 lines
38 KiB
JavaScript
1038 lines
38 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.testTypeVersion = exports.testTypeExVer = exports.ExtendedVersion = exports.Version = exports.VersionRange = void 0;
|
|
const deep_equality_data_structures_1 = require("deep-equality-data-structures");
|
|
const P = __importStar(require("./exver"));
|
|
function compareVersionRangePoints(a, b) {
|
|
let up = a.upstream.compareForSort(b.upstream);
|
|
if (up != 0) {
|
|
return up;
|
|
}
|
|
let down = a.upstream.compareForSort(b.upstream);
|
|
if (down != 0) {
|
|
return down;
|
|
}
|
|
if (a.side < b.side) {
|
|
return -1;
|
|
}
|
|
else if (a.side > b.side) {
|
|
return 1;
|
|
}
|
|
else {
|
|
return 0;
|
|
}
|
|
}
|
|
function adjacentVersionRangePoints(a, b) {
|
|
let up = a.upstream.compareForSort(b.upstream);
|
|
if (up != 0) {
|
|
return false;
|
|
}
|
|
let down = a.upstream.compareForSort(b.upstream);
|
|
if (down != 0) {
|
|
return false;
|
|
}
|
|
return a.side == -1 && b.side == 1;
|
|
}
|
|
function flavorAnd(a, b) {
|
|
if (a.type == 'Flavor') {
|
|
if (b.type == 'Flavor') {
|
|
if (a.flavor == b.flavor) {
|
|
return a;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
else {
|
|
if (b.flavors.has(a.flavor)) {
|
|
return null;
|
|
}
|
|
else {
|
|
return a;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (b.type == 'Flavor') {
|
|
if (a.flavors.has(b.flavor)) {
|
|
return null;
|
|
}
|
|
else {
|
|
return b;
|
|
}
|
|
}
|
|
else {
|
|
// TODO: use Set.union if targeting esnext or later
|
|
return {
|
|
type: 'FlavorNot',
|
|
flavors: new Set([...a.flavors, ...b.flavors]),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* A truth table for version numbers. This is easiest to picture as a number line, cut up into
|
|
* ranges of versions between version points.
|
|
*/
|
|
class VersionRangeTable {
|
|
constructor(points, values) {
|
|
this.points = points;
|
|
this.values = values;
|
|
}
|
|
static zip(a, b, func) {
|
|
let c = new VersionRangeTable([], []);
|
|
let i = 0;
|
|
let j = 0;
|
|
while (true) {
|
|
let next = func(a.values[i], b.values[j]);
|
|
if (c.values.length > 0 && c.values[c.values.length - 1] == next) {
|
|
// collapse automatically
|
|
c.points.pop();
|
|
}
|
|
else {
|
|
c.values.push(next);
|
|
}
|
|
// which point do we step over?
|
|
if (i == a.points.length) {
|
|
if (j == b.points.length) {
|
|
// just added the last segment, no point to jump over
|
|
return c;
|
|
}
|
|
else {
|
|
// i has reach the end, step over j
|
|
c.points.push(b.points[j]);
|
|
j += 1;
|
|
}
|
|
}
|
|
else {
|
|
if (j == b.points.length) {
|
|
// j has reached the end, step over i
|
|
c.points.push(a.points[i]);
|
|
i += 1;
|
|
}
|
|
else {
|
|
// depends on which of the next two points is lower
|
|
switch (compareVersionRangePoints(a.points[i], b.points[j])) {
|
|
case -1:
|
|
// i is the lower point
|
|
c.points.push(a.points[i]);
|
|
i += 1;
|
|
break;
|
|
case 1:
|
|
// j is the lower point
|
|
c.points.push(b.points[j]);
|
|
j += 1;
|
|
break;
|
|
default:
|
|
// step over both
|
|
c.points.push(a.points[i]);
|
|
i += 1;
|
|
j += 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Creates a version table which is `true` for the given flavor, and `false` for any other flavor.
|
|
*/
|
|
static eqFlavor(flavor) {
|
|
return new deep_equality_data_structures_1.DeepMap([
|
|
[
|
|
{ type: 'Flavor', flavor },
|
|
new VersionRangeTable([], [true]),
|
|
],
|
|
// make sure the truth table is exhaustive, or `not` will not work properly.
|
|
[
|
|
{ type: 'FlavorNot', flavors: new Set([flavor]) },
|
|
new VersionRangeTable([], [false]),
|
|
],
|
|
]);
|
|
}
|
|
/**
|
|
* Creates a version table with exactly two ranges (to the left and right of the given point) and with `false` for any other flavor.
|
|
* This is easiest to understand by looking at `VersionRange.tables`.
|
|
*/
|
|
static cmpPoint(flavor, point, left, right) {
|
|
return new deep_equality_data_structures_1.DeepMap([
|
|
[
|
|
{ type: 'Flavor', flavor },
|
|
new VersionRangeTable([point], [left, right]),
|
|
],
|
|
// make sure the truth table is exhaustive, or `not` will not work properly.
|
|
[
|
|
{ type: 'FlavorNot', flavors: new Set([flavor]) },
|
|
new VersionRangeTable([], [false]),
|
|
],
|
|
]);
|
|
}
|
|
/**
|
|
* Helper for `cmpPoint`.
|
|
*/
|
|
static cmp(version, side, left, right) {
|
|
return VersionRangeTable.cmpPoint(version.flavor, { upstream: version.upstream, downstream: version.downstream, side }, left, right);
|
|
}
|
|
static not(tables) {
|
|
if (tables === true || tables === false) {
|
|
return !tables;
|
|
}
|
|
// because tables are always exhaustive, we can simply invert each range
|
|
for (let [f, t] of tables) {
|
|
for (let i = 0; i < t.values.length; i++) {
|
|
t.values[i] = !t.values[i];
|
|
}
|
|
}
|
|
return tables;
|
|
}
|
|
static and(a_tables, b_tables) {
|
|
if (a_tables === true) {
|
|
return b_tables;
|
|
}
|
|
if (b_tables === true) {
|
|
return a_tables;
|
|
}
|
|
if (a_tables === false || b_tables == false) {
|
|
return false;
|
|
}
|
|
let c_tables = true;
|
|
for (let [f_a, a] of a_tables) {
|
|
for (let [f_b, b] of b_tables) {
|
|
let flavor = flavorAnd(f_a, f_b);
|
|
if (flavor == null) {
|
|
continue;
|
|
}
|
|
let c = VersionRangeTable.zip(a, b, (a, b) => a && b);
|
|
if (c_tables === true) {
|
|
c_tables = new deep_equality_data_structures_1.DeepMap();
|
|
}
|
|
let prev_c = c_tables.get(flavor);
|
|
if (prev_c == null) {
|
|
c_tables.set(flavor, c);
|
|
}
|
|
else {
|
|
c_tables.set(flavor, VersionRangeTable.zip(c, prev_c, (a, b) => a || b));
|
|
}
|
|
}
|
|
}
|
|
return c_tables;
|
|
}
|
|
static or(...in_tables) {
|
|
let out_tables = false;
|
|
for (let tables of in_tables) {
|
|
if (tables === false) {
|
|
continue;
|
|
}
|
|
if (tables === true) {
|
|
return true;
|
|
}
|
|
if (out_tables === false) {
|
|
out_tables = new deep_equality_data_structures_1.DeepMap();
|
|
}
|
|
for (let [flavor, table] of tables) {
|
|
let prev = out_tables.get(flavor);
|
|
if (prev == null) {
|
|
out_tables.set(flavor, table);
|
|
}
|
|
else {
|
|
out_tables.set(flavor, VersionRangeTable.zip(table, prev, (a, b) => a || b));
|
|
}
|
|
}
|
|
}
|
|
return out_tables;
|
|
}
|
|
/**
|
|
* If this is true for all versions or false for all versions, returen that value. Otherwise return null.
|
|
*/
|
|
static collapse(tables) {
|
|
if (tables === true || tables === false) {
|
|
return tables;
|
|
}
|
|
else {
|
|
let found = null;
|
|
for (let table of tables.values()) {
|
|
for (let x of table.values) {
|
|
if (found == null) {
|
|
found = x;
|
|
}
|
|
else if (found != x) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
}
|
|
/**
|
|
* Expresses this truth table as a series of version range operators.
|
|
* https://en.wikipedia.org/wiki/Canonical_normal_form#Minterms
|
|
*/
|
|
static minterms(tables) {
|
|
let collapse = VersionRangeTable.collapse(tables);
|
|
if (tables === true || collapse === true) {
|
|
return VersionRange.any();
|
|
}
|
|
if (tables == false || collapse === false) {
|
|
return VersionRange.none();
|
|
}
|
|
let sum_terms = [];
|
|
for (let [flavor, table] of tables) {
|
|
let cmp_flavor = null;
|
|
if (flavor.type == 'Flavor') {
|
|
cmp_flavor = flavor.flavor;
|
|
}
|
|
for (let i = 0; i < table.values.length; i++) {
|
|
let term = [];
|
|
if (!table.values[i]) {
|
|
continue;
|
|
}
|
|
if (flavor.type == 'FlavorNot') {
|
|
for (let not_flavor of flavor.flavors) {
|
|
term.push(VersionRange.flavor(not_flavor).not());
|
|
}
|
|
}
|
|
let p = null;
|
|
let q = null;
|
|
if (i > 0) {
|
|
p = table.points[i - 1];
|
|
}
|
|
if (i < table.points.length) {
|
|
q = table.points[i];
|
|
}
|
|
if (p != null && q != null && adjacentVersionRangePoints(p, q)) {
|
|
term.push(VersionRange.anchor('=', new ExtendedVersion(cmp_flavor, p.upstream, p.downstream)));
|
|
}
|
|
else {
|
|
if (p != null && p.side < 0) {
|
|
term.push(VersionRange.anchor('>=', new ExtendedVersion(cmp_flavor, p.upstream, p.downstream)));
|
|
}
|
|
if (p != null && p.side >= 0) {
|
|
term.push(VersionRange.anchor('>', new ExtendedVersion(cmp_flavor, p.upstream, p.downstream)));
|
|
}
|
|
if (q != null && q.side < 0) {
|
|
term.push(VersionRange.anchor('<', new ExtendedVersion(cmp_flavor, q.upstream, q.downstream)));
|
|
}
|
|
if (q != null && q.side >= 0) {
|
|
term.push(VersionRange.anchor('<=', new ExtendedVersion(cmp_flavor, q.upstream, q.downstream)));
|
|
}
|
|
}
|
|
if (term.length == 0) {
|
|
term.push(VersionRange.flavor(cmp_flavor));
|
|
}
|
|
sum_terms.push(VersionRange.and(...term));
|
|
}
|
|
}
|
|
return VersionRange.or(...sum_terms);
|
|
}
|
|
}
|
|
/**
|
|
* Represents a parsed version range expression used to match against {@link Version} or {@link ExtendedVersion} values.
|
|
*
|
|
* Version ranges support standard comparison operators (`=`, `>`, `<`, `>=`, `<=`, `!=`),
|
|
* caret (`^`) and tilde (`~`) ranges, boolean logic (`&&`, `||`, `!`), and flavor matching (`#flavor`).
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const range = VersionRange.parse(">=1.0.0:0 && <2.0.0:0")
|
|
* const version = ExtendedVersion.parse("1.5.0:0")
|
|
* console.log(range.satisfiedBy(version)) // true
|
|
*
|
|
* // Combine ranges with boolean logic
|
|
* const combined = VersionRange.and(
|
|
* VersionRange.parse(">=1.0:0"),
|
|
* VersionRange.parse("<3.0:0"),
|
|
* )
|
|
*
|
|
* // Match a specific flavor
|
|
* const flavored = VersionRange.parse("#bitcoin")
|
|
* ```
|
|
*/
|
|
class VersionRange {
|
|
constructor(atom) {
|
|
this.atom = atom;
|
|
}
|
|
toStringParens(parent) {
|
|
let needs = true;
|
|
switch (this.atom.type) {
|
|
case 'And':
|
|
case 'Or':
|
|
needs = parent != this.atom.type;
|
|
break;
|
|
case 'Anchor':
|
|
case 'Any':
|
|
case 'None':
|
|
needs = parent == 'Not';
|
|
break;
|
|
case 'Not':
|
|
case 'Flavor':
|
|
needs = false;
|
|
break;
|
|
}
|
|
if (needs) {
|
|
return '(' + this.toString() + ')';
|
|
}
|
|
else {
|
|
return this.toString();
|
|
}
|
|
}
|
|
/** Serializes this version range back to its canonical string representation. */
|
|
toString() {
|
|
switch (this.atom.type) {
|
|
case 'Anchor':
|
|
return `${this.atom.operator}${this.atom.version}`;
|
|
case 'And':
|
|
return `${this.atom.left.toStringParens(this.atom.type)} && ${this.atom.right.toStringParens(this.atom.type)}`;
|
|
case 'Or':
|
|
return `${this.atom.left.toStringParens(this.atom.type)} || ${this.atom.right.toStringParens(this.atom.type)}`;
|
|
case 'Not':
|
|
return `!${this.atom.value.toStringParens(this.atom.type)}`;
|
|
case 'Flavor':
|
|
return this.atom.flavor == null ? `#` : `#${this.atom.flavor}`;
|
|
case 'Any':
|
|
return '*';
|
|
case 'None':
|
|
return '!';
|
|
}
|
|
}
|
|
static parseAtom(atom) {
|
|
switch (atom.type) {
|
|
case 'Not':
|
|
return new VersionRange({
|
|
type: 'Not',
|
|
value: VersionRange.parseAtom(atom.value),
|
|
});
|
|
case 'Parens':
|
|
return VersionRange.parseRange(atom.expr);
|
|
case 'Anchor':
|
|
return new VersionRange({
|
|
type: 'Anchor',
|
|
operator: atom.operator || '^',
|
|
version: new ExtendedVersion(atom.version.flavor, new Version(atom.version.upstream.number, atom.version.upstream.prerelease), new Version(atom.version.downstream.number, atom.version.downstream.prerelease)),
|
|
});
|
|
case 'Flavor':
|
|
return VersionRange.flavor(atom.flavor);
|
|
default:
|
|
return new VersionRange(atom);
|
|
}
|
|
}
|
|
static parseRange(range) {
|
|
let result = VersionRange.parseAtom(range[0]);
|
|
for (const next of range[1]) {
|
|
switch (next[1]?.[0]) {
|
|
case '||':
|
|
result = new VersionRange({
|
|
type: 'Or',
|
|
left: result,
|
|
right: VersionRange.parseAtom(next[2]),
|
|
});
|
|
break;
|
|
case '&&':
|
|
default:
|
|
result = new VersionRange({
|
|
type: 'And',
|
|
left: result,
|
|
right: VersionRange.parseAtom(next[2]),
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* Parses a version range string into a `VersionRange`.
|
|
*
|
|
* @param range - A version range expression, e.g. `">=1.0.0:0 && <2.0.0:0"`, `"^1.2:0"`, `"*"`
|
|
* @returns The parsed `VersionRange`
|
|
* @throws If the string is not a valid version range expression
|
|
*/
|
|
static parse(range) {
|
|
return VersionRange.parseRange(P.parse(range, { startRule: 'VersionRange' }));
|
|
}
|
|
/**
|
|
* Creates a version range from a comparison operator and an {@link ExtendedVersion}.
|
|
*
|
|
* @param operator - One of `"="`, `">"`, `"<"`, `">="`, `"<="`, `"!="`, `"^"`, `"~"`
|
|
* @param version - The version to compare against
|
|
*/
|
|
static anchor(operator, version) {
|
|
return new VersionRange({ type: 'Anchor', operator, version });
|
|
}
|
|
/**
|
|
* Creates a version range that matches only versions with the specified flavor.
|
|
*
|
|
* @param flavor - The flavor string to match, or `null` for the default (unflavored) variant
|
|
*/
|
|
static flavor(flavor) {
|
|
return new VersionRange({ type: 'Flavor', flavor });
|
|
}
|
|
/**
|
|
* Parses a legacy "emver" format version range string.
|
|
*
|
|
* @param range - A version range in the legacy emver format
|
|
* @returns The parsed `VersionRange`
|
|
*/
|
|
static parseEmver(range) {
|
|
return VersionRange.parseRange(P.parse(range, { startRule: 'EmverVersionRange' }));
|
|
}
|
|
/** Returns the intersection of this range with another (logical AND). */
|
|
and(right) {
|
|
return new VersionRange({ type: 'And', left: this, right });
|
|
}
|
|
/** Returns the union of this range with another (logical OR). */
|
|
or(right) {
|
|
return new VersionRange({ type: 'Or', left: this, right });
|
|
}
|
|
/** Returns the negation of this range (logical NOT). */
|
|
not() {
|
|
return new VersionRange({ type: 'Not', value: this });
|
|
}
|
|
/**
|
|
* Returns the logical AND (intersection) of multiple version ranges.
|
|
* Short-circuits on `none()` and skips `any()`.
|
|
*/
|
|
static and(...xs) {
|
|
let y = VersionRange.any();
|
|
for (let x of xs) {
|
|
if (x.atom.type == 'Any') {
|
|
continue;
|
|
}
|
|
if (x.atom.type == 'None') {
|
|
return x;
|
|
}
|
|
if (y.atom.type == 'Any') {
|
|
y = x;
|
|
}
|
|
else {
|
|
y = new VersionRange({ type: 'And', left: y, right: x });
|
|
}
|
|
}
|
|
return y;
|
|
}
|
|
/**
|
|
* Returns the logical OR (union) of multiple version ranges.
|
|
* Short-circuits on `any()` and skips `none()`.
|
|
*/
|
|
static or(...xs) {
|
|
let y = VersionRange.none();
|
|
for (let x of xs) {
|
|
if (x.atom.type == 'None') {
|
|
continue;
|
|
}
|
|
if (x.atom.type == 'Any') {
|
|
return x;
|
|
}
|
|
if (y.atom.type == 'None') {
|
|
y = x;
|
|
}
|
|
else {
|
|
y = new VersionRange({ type: 'Or', left: y, right: x });
|
|
}
|
|
}
|
|
return y;
|
|
}
|
|
/** Returns a version range that matches all versions (wildcard `*`). */
|
|
static any() {
|
|
return new VersionRange({ type: 'Any' });
|
|
}
|
|
/** Returns a version range that matches no versions (`!`). */
|
|
static none() {
|
|
return new VersionRange({ type: 'None' });
|
|
}
|
|
/**
|
|
* Returns `true` if the given version satisfies this range.
|
|
*
|
|
* @param version - A {@link Version} or {@link ExtendedVersion} to test
|
|
*/
|
|
satisfiedBy(version) {
|
|
return version.satisfies(this);
|
|
}
|
|
tables() {
|
|
switch (this.atom.type) {
|
|
case 'Anchor':
|
|
switch (this.atom.operator) {
|
|
case '=':
|
|
// `=1.2.3` is equivalent to `>=1.2.3 && <=1.2.4 && #flavor`
|
|
return VersionRangeTable.and(VersionRangeTable.cmp(this.atom.version, -1, false, true), VersionRangeTable.cmp(this.atom.version, 1, true, false));
|
|
case '>':
|
|
return VersionRangeTable.cmp(this.atom.version, 1, false, true);
|
|
case '<':
|
|
return VersionRangeTable.cmp(this.atom.version, -1, true, false);
|
|
case '>=':
|
|
return VersionRangeTable.cmp(this.atom.version, -1, false, true);
|
|
case '<=':
|
|
return VersionRangeTable.cmp(this.atom.version, 1, true, false);
|
|
case '!=':
|
|
// `!=1.2.3` is equivalent to `!(>=1.2.3 && <=1.2.3 && #flavor)`
|
|
// **not** equivalent to `(<1.2.3 || >1.2.3) && #flavor`
|
|
return VersionRangeTable.not(VersionRangeTable.and(VersionRangeTable.cmp(this.atom.version, -1, false, true), VersionRangeTable.cmp(this.atom.version, 1, true, false)));
|
|
case '^':
|
|
// `^1.2.3` is equivalent to `>=1.2.3 && <2.0.0 && #flavor`
|
|
return VersionRangeTable.and(VersionRangeTable.cmp(this.atom.version, -1, false, true), VersionRangeTable.cmp(this.atom.version.incrementMajor(), -1, true, false));
|
|
case '~':
|
|
// `~1.2.3` is equivalent to `>=1.2.3 && <1.3.0 && #flavor`
|
|
return VersionRangeTable.and(VersionRangeTable.cmp(this.atom.version, -1, false, true), VersionRangeTable.cmp(this.atom.version.incrementMinor(), -1, true, false));
|
|
}
|
|
case 'Flavor':
|
|
return VersionRangeTable.eqFlavor(this.atom.flavor);
|
|
case 'Not':
|
|
return VersionRangeTable.not(this.atom.value.tables());
|
|
case 'And':
|
|
return VersionRangeTable.and(this.atom.left.tables(), this.atom.right.tables());
|
|
case 'Or':
|
|
return VersionRangeTable.or(this.atom.left.tables(), this.atom.right.tables());
|
|
case 'Any':
|
|
return true;
|
|
case 'None':
|
|
return false;
|
|
}
|
|
}
|
|
/** Returns `true` if any version exists that could satisfy this range. */
|
|
satisfiable() {
|
|
return VersionRangeTable.collapse(this.tables()) !== false;
|
|
}
|
|
/** Returns `true` if this range and `other` share at least one satisfying version. */
|
|
intersects(other) {
|
|
return VersionRange.and(this, other).satisfiable();
|
|
}
|
|
/**
|
|
* Returns a canonical (simplified) form of this range using minterm expansion.
|
|
* Useful for normalizing complex boolean expressions into a minimal representation.
|
|
*/
|
|
normalize() {
|
|
return VersionRangeTable.minterms(this.tables());
|
|
}
|
|
}
|
|
exports.VersionRange = VersionRange;
|
|
/**
|
|
* Represents a semantic version number with numeric segments and optional prerelease identifiers.
|
|
*
|
|
* Follows semver precedence rules: numeric segments are compared left-to-right,
|
|
* and a version with prerelease identifiers has lower precedence than the same version without.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const v = Version.parse("1.2.3")
|
|
* console.log(v.toString()) // "1.2.3"
|
|
* console.log(v.compare(Version.parse("1.3.0"))) // "less"
|
|
*
|
|
* const pre = Version.parse("2.0.0-beta.1")
|
|
* console.log(pre.compare(Version.parse("2.0.0"))) // "less" (prerelease < release)
|
|
* ```
|
|
*/
|
|
class Version {
|
|
constructor(
|
|
/** The numeric version segments (e.g. `[1, 2, 3]` for `"1.2.3"`). */
|
|
number,
|
|
/** Optional prerelease identifiers (e.g. `["beta", 1]` for `"-beta.1"`). */
|
|
prerelease) {
|
|
this.number = number;
|
|
this.prerelease = prerelease;
|
|
}
|
|
/** Serializes this version to its string form (e.g. `"1.2.3"` or `"1.0.0-beta.1"`). */
|
|
toString() {
|
|
return `${this.number.join('.')}${this.prerelease.length > 0 ? `-${this.prerelease.join('.')}` : ''}`;
|
|
}
|
|
/**
|
|
* Compares this version against another using semver precedence rules.
|
|
*
|
|
* @param other - The version to compare against
|
|
* @returns `'greater'`, `'equal'`, or `'less'`
|
|
*/
|
|
compare(other) {
|
|
const numLen = Math.max(this.number.length, other.number.length);
|
|
for (let i = 0; i < numLen; i++) {
|
|
if ((this.number[i] || 0) > (other.number[i] || 0)) {
|
|
return 'greater';
|
|
}
|
|
else if ((this.number[i] || 0) < (other.number[i] || 0)) {
|
|
return 'less';
|
|
}
|
|
}
|
|
if (this.prerelease.length === 0 && other.prerelease.length !== 0) {
|
|
return 'greater';
|
|
}
|
|
else if (this.prerelease.length !== 0 && other.prerelease.length === 0) {
|
|
return 'less';
|
|
}
|
|
const prereleaseLen = Math.max(this.prerelease.length, other.prerelease.length);
|
|
for (let i = 0; i < prereleaseLen; i++) {
|
|
if (typeof this.prerelease[i] === typeof other.prerelease[i]) {
|
|
if (this.prerelease[i] > other.prerelease[i]) {
|
|
return 'greater';
|
|
}
|
|
else if (this.prerelease[i] < other.prerelease[i]) {
|
|
return 'less';
|
|
}
|
|
}
|
|
else {
|
|
switch (`${typeof this.prerelease[1]}:${typeof other.prerelease[i]}`) {
|
|
case 'number:string':
|
|
return 'less';
|
|
case 'string:number':
|
|
return 'greater';
|
|
case 'number:undefined':
|
|
case 'string:undefined':
|
|
return 'greater';
|
|
case 'undefined:number':
|
|
case 'undefined:string':
|
|
return 'less';
|
|
}
|
|
}
|
|
}
|
|
return 'equal';
|
|
}
|
|
/**
|
|
* Compares two versions, returning a numeric value suitable for use with `Array.sort()`.
|
|
*
|
|
* @returns `-1` if less, `0` if equal, `1` if greater
|
|
*/
|
|
compareForSort(other) {
|
|
switch (this.compare(other)) {
|
|
case 'greater':
|
|
return 1;
|
|
case 'equal':
|
|
return 0;
|
|
case 'less':
|
|
return -1;
|
|
}
|
|
}
|
|
/**
|
|
* Parses a version string into a `Version` instance.
|
|
*
|
|
* @param version - A semver-compatible string, e.g. `"1.2.3"` or `"1.0.0-beta.1"`
|
|
* @throws If the string is not a valid version
|
|
*/
|
|
static parse(version) {
|
|
const parsed = P.parse(version, { startRule: 'Version' });
|
|
return new Version(parsed.number, parsed.prerelease);
|
|
}
|
|
/**
|
|
* Returns `true` if this version satisfies the given {@link VersionRange}.
|
|
* Internally treats this as an unflavored {@link ExtendedVersion} with downstream `0`.
|
|
*/
|
|
satisfies(versionRange) {
|
|
return new ExtendedVersion(null, this, new Version([0], [])).satisfies(versionRange);
|
|
}
|
|
}
|
|
exports.Version = Version;
|
|
/**
|
|
* Represents an extended version with an optional flavor, an upstream version, and a downstream version.
|
|
*
|
|
* The format is `#flavor:upstream:downstream` (e.g. `#bitcoin:1.2.3:0`) or `upstream:downstream`
|
|
* for unflavored versions. Flavors allow multiple variants of a package to coexist.
|
|
*
|
|
* - **flavor**: An optional string identifier for the variant (e.g. `"bitcoin"`, `"litecoin"`)
|
|
* - **upstream**: The version of the upstream software being packaged
|
|
* - **downstream**: The version of the StartOS packaging itself
|
|
*
|
|
* Versions with different flavors are incomparable (comparison returns `null`).
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const v = ExtendedVersion.parse("#bitcoin:1.2.3:0")
|
|
* console.log(v.flavor) // "bitcoin"
|
|
* console.log(v.upstream) // Version { number: [1, 2, 3] }
|
|
* console.log(v.downstream) // Version { number: [0] }
|
|
* console.log(v.toString()) // "#bitcoin:1.2.3:0"
|
|
*
|
|
* const range = VersionRange.parse(">=1.0.0:0")
|
|
* console.log(v.satisfies(range)) // true
|
|
* ```
|
|
*/
|
|
class ExtendedVersion {
|
|
constructor(
|
|
/** The flavor identifier (e.g. `"bitcoin"`), or `null` for unflavored versions. */
|
|
flavor,
|
|
/** The upstream software version. */
|
|
upstream,
|
|
/** The downstream packaging version. */
|
|
downstream) {
|
|
this.flavor = flavor;
|
|
this.upstream = upstream;
|
|
this.downstream = downstream;
|
|
}
|
|
/** Serializes this extended version to its string form (e.g. `"#bitcoin:1.2.3:0"` or `"1.0.0:1"`). */
|
|
toString() {
|
|
return `${this.flavor ? `#${this.flavor}:` : ''}${this.upstream.toString()}:${this.downstream.toString()}`;
|
|
}
|
|
/**
|
|
* Compares this extended version against another.
|
|
*
|
|
* @returns `'greater'`, `'equal'`, `'less'`, or `null` if the flavors differ (incomparable)
|
|
*/
|
|
compare(other) {
|
|
if (this.flavor !== other.flavor) {
|
|
return null;
|
|
}
|
|
const upstreamCmp = this.upstream.compare(other.upstream);
|
|
if (upstreamCmp !== 'equal') {
|
|
return upstreamCmp;
|
|
}
|
|
return this.downstream.compare(other.downstream);
|
|
}
|
|
/**
|
|
* Lexicographic comparison — compares flavors alphabetically first, then versions.
|
|
* Unlike {@link compare}, this never returns `null`: different flavors are ordered alphabetically.
|
|
*/
|
|
compareLexicographic(other) {
|
|
if ((this.flavor || '') > (other.flavor || '')) {
|
|
return 'greater';
|
|
}
|
|
else if ((this.flavor || '') > (other.flavor || '')) {
|
|
return 'less';
|
|
}
|
|
else {
|
|
return this.compare(other);
|
|
}
|
|
}
|
|
/**
|
|
* Returns a numeric comparison result suitable for use with `Array.sort()`.
|
|
* Uses lexicographic ordering (flavors sorted alphabetically, then by version).
|
|
*/
|
|
compareForSort(other) {
|
|
switch (this.compareLexicographic(other)) {
|
|
case 'greater':
|
|
return 1;
|
|
case 'equal':
|
|
return 0;
|
|
case 'less':
|
|
return -1;
|
|
}
|
|
}
|
|
/** Returns `true` if this version is strictly greater than `other`. Returns `false` if flavors differ. */
|
|
greaterThan(other) {
|
|
return this.compare(other) === 'greater';
|
|
}
|
|
/** Returns `true` if this version is greater than or equal to `other`. Returns `false` if flavors differ. */
|
|
greaterThanOrEqual(other) {
|
|
return ['greater', 'equal'].includes(this.compare(other));
|
|
}
|
|
/** Returns `true` if this version equals `other` (same flavor, upstream, and downstream). */
|
|
equals(other) {
|
|
return this.compare(other) === 'equal';
|
|
}
|
|
/** Returns `true` if this version is strictly less than `other`. Returns `false` if flavors differ. */
|
|
lessThan(other) {
|
|
return this.compare(other) === 'less';
|
|
}
|
|
/** Returns `true` if this version is less than or equal to `other`. Returns `false` if flavors differ. */
|
|
lessThanOrEqual(other) {
|
|
return ['less', 'equal'].includes(this.compare(other));
|
|
}
|
|
/**
|
|
* Parses an extended version string into an `ExtendedVersion`.
|
|
*
|
|
* @param extendedVersion - A string like `"1.2.3:0"` or `"#bitcoin:1.0.0:0"`
|
|
* @throws If the string is not a valid extended version
|
|
*/
|
|
static parse(extendedVersion) {
|
|
const parsed = P.parse(extendedVersion, { startRule: 'ExtendedVersion' });
|
|
return new ExtendedVersion(parsed.flavor || null, new Version(parsed.upstream.number, parsed.upstream.prerelease), new Version(parsed.downstream.number, parsed.downstream.prerelease));
|
|
}
|
|
/**
|
|
* Parses a legacy "emver" format extended version string.
|
|
*
|
|
* @param extendedVersion - A version string in the legacy emver format
|
|
* @throws If the string is not a valid emver version (error message includes the input string)
|
|
*/
|
|
static parseEmver(extendedVersion) {
|
|
try {
|
|
const parsed = P.parse(extendedVersion, { startRule: 'Emver' });
|
|
return new ExtendedVersion(parsed.flavor || null, new Version(parsed.upstream.number, parsed.upstream.prerelease), new Version(parsed.downstream.number, parsed.downstream.prerelease));
|
|
}
|
|
catch (e) {
|
|
if (e instanceof Error) {
|
|
e.message += ` (${extendedVersion})`;
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
/**
|
|
* Returns an ExtendedVersion with the Upstream major version version incremented by 1
|
|
* and sets subsequent digits to zero.
|
|
* If no non-zero upstream digit can be found the last upstream digit will be incremented.
|
|
*/
|
|
incrementMajor() {
|
|
const majorIdx = this.upstream.number.findIndex((num) => num !== 0);
|
|
const majorNumber = this.upstream.number.map((num, idx) => {
|
|
if (idx > majorIdx) {
|
|
return 0;
|
|
}
|
|
else if (idx === majorIdx) {
|
|
return num + 1;
|
|
}
|
|
return num;
|
|
});
|
|
const incrementedUpstream = new Version(majorNumber, []);
|
|
const updatedDownstream = new Version([0], []);
|
|
return new ExtendedVersion(this.flavor, incrementedUpstream, updatedDownstream);
|
|
}
|
|
/**
|
|
* Returns an ExtendedVersion with the Upstream minor version version incremented by 1
|
|
* also sets subsequent digits to zero.
|
|
* If no non-zero upstream digit can be found the last digit will be incremented.
|
|
*/
|
|
incrementMinor() {
|
|
const majorIdx = this.upstream.number.findIndex((num) => num !== 0);
|
|
let minorIdx = majorIdx === -1 ? majorIdx : majorIdx + 1;
|
|
const majorNumber = this.upstream.number.map((num, idx) => {
|
|
if (idx > minorIdx) {
|
|
return 0;
|
|
}
|
|
else if (idx === minorIdx) {
|
|
return num + 1;
|
|
}
|
|
return num;
|
|
});
|
|
const incrementedUpstream = new Version(majorNumber, []);
|
|
const updatedDownstream = new Version([0], []);
|
|
return new ExtendedVersion(this.flavor, incrementedUpstream, updatedDownstream);
|
|
}
|
|
/**
|
|
* Returns a boolean indicating whether a given version satisfies the VersionRange
|
|
* !( >= 1:1 <= 2:2) || <=#bitcoin:1.2.0-alpha:0
|
|
*/
|
|
satisfies(versionRange) {
|
|
switch (versionRange.atom.type) {
|
|
case 'Anchor':
|
|
const otherVersion = versionRange.atom.version;
|
|
switch (versionRange.atom.operator) {
|
|
case '=':
|
|
return this.equals(otherVersion);
|
|
case '>':
|
|
return this.greaterThan(otherVersion);
|
|
case '<':
|
|
return this.lessThan(otherVersion);
|
|
case '>=':
|
|
return this.greaterThanOrEqual(otherVersion);
|
|
case '<=':
|
|
return this.lessThanOrEqual(otherVersion);
|
|
case '!=':
|
|
return !this.equals(otherVersion);
|
|
case '^':
|
|
const nextMajor = versionRange.atom.version.incrementMajor();
|
|
if (this.greaterThanOrEqual(otherVersion) &&
|
|
this.lessThan(nextMajor)) {
|
|
return true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
case '~':
|
|
const nextMinor = versionRange.atom.version.incrementMinor();
|
|
if (this.greaterThanOrEqual(otherVersion) &&
|
|
this.lessThan(nextMinor)) {
|
|
return true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
case 'Flavor':
|
|
return versionRange.atom.flavor == this.flavor;
|
|
case 'And':
|
|
return (this.satisfies(versionRange.atom.left) &&
|
|
this.satisfies(versionRange.atom.right));
|
|
case 'Or':
|
|
return (this.satisfies(versionRange.atom.left) ||
|
|
this.satisfies(versionRange.atom.right));
|
|
case 'Not':
|
|
return !this.satisfies(versionRange.atom.value);
|
|
case 'Any':
|
|
return true;
|
|
case 'None':
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
exports.ExtendedVersion = ExtendedVersion;
|
|
/**
|
|
* Compile-time type-checking helper that validates an extended version string literal.
|
|
* If the string is invalid, TypeScript will report a type error at the call site.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* testTypeExVer("1.2.3:0") // compiles
|
|
* testTypeExVer("#bitcoin:1.0:0") // compiles
|
|
* testTypeExVer("invalid") // type error
|
|
* ```
|
|
*/
|
|
const testTypeExVer = (t) => t;
|
|
exports.testTypeExVer = testTypeExVer;
|
|
/**
|
|
* Compile-time type-checking helper that validates a version string literal.
|
|
* If the string is invalid, TypeScript will report a type error at the call site.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* testTypeVersion("1.2.3") // compiles
|
|
* testTypeVersion("-3") // type error
|
|
* ```
|
|
*/
|
|
const testTypeVersion = (t) => t;
|
|
exports.testTypeVersion = testTypeVersion;
|
|
function tests() {
|
|
(0, exports.testTypeVersion)('1.2.3');
|
|
(0, exports.testTypeVersion)('1');
|
|
(0, exports.testTypeVersion)('12.34.56');
|
|
(0, exports.testTypeVersion)('1.2-3');
|
|
(0, exports.testTypeVersion)('1-3');
|
|
(0, exports.testTypeVersion)('1-alpha');
|
|
// @ts-expect-error
|
|
(0, exports.testTypeVersion)('-3');
|
|
// @ts-expect-error
|
|
(0, exports.testTypeVersion)('1.2.3:1');
|
|
// @ts-expect-error
|
|
(0, exports.testTypeVersion)('#cat:1:1');
|
|
(0, exports.testTypeExVer)('1.2.3:1.2.3');
|
|
(0, exports.testTypeExVer)('1.2.3.4.5.6.7.8.9.0:1');
|
|
(0, exports.testTypeExVer)('100:1');
|
|
(0, exports.testTypeExVer)('#cat:1:1');
|
|
(0, exports.testTypeExVer)('1.2.3.4.5.6.7.8.9.11.22.33:1');
|
|
(0, exports.testTypeExVer)('1-0:1');
|
|
(0, exports.testTypeExVer)('1-0:1');
|
|
// @ts-expect-error
|
|
(0, exports.testTypeExVer)('1.2-3');
|
|
// @ts-expect-error
|
|
(0, exports.testTypeExVer)('1-3');
|
|
// @ts-expect-error
|
|
(0, exports.testTypeExVer)('1.2.3.4.5.6.7.8.9.0.10:1');
|
|
// @ts-expect-error
|
|
(0, exports.testTypeExVer)('1.-2:1');
|
|
// @ts-expect-error
|
|
(0, exports.testTypeExVer)('1..2.3:3');
|
|
}
|
|
//# sourceMappingURL=index.js.map
|