mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
* bugfixes * update fe types * implement new registry types in marketplace and ui * fix marketplace types to have default params * add alt implementation toggle * merge cleanup * more cleanup and notes * fix build * cleanup sync with next/minor * add exver JS parser * parse ValidExVer to string * update types to interface * add VersionRange and comparative functions * Parse ExtendedVersion from string * add conjunction, disjunction, and inversion logic * consider flavor in satisfiedBy fn * consider prerelease for ordering * add compare fn for sorting * rename fns for consistency * refactoring * update compare fn to return null if flavors don't match * begin simplifying dependencies * under construction * wip * add dependency metadata to CurrentDependencyInfo * ditch inheritance for recursive VersionRange constructor. Recursive 'satisfiedBy' fn wip * preprocess manifest * misc fixes * use sdk version as osVersion in manifest * chore: Change the type to just validate and not generate all solutions. * add publishedAt * fix pegjs exports * integrate exver into sdk * misc fixes * complete satisfiedBy fn * refactor - use greaterThanOrEqual and lessThanOrEqual fns * fix tests * update dependency details * update types * remove interim types * rename alt implementation to flavor * cleanup os update * format exver.ts * add s9pk parsing endpoints * fix build * update to exver * exver and bug fixes * update static endpoints + cleanup * cleanup * update static proxy verification * make mocks more robust; fix dep icon fallback; cleanup * refactor alert versions and update fixtures * registry bugfixes * misc fixes * cleanup unused * convert patchdb ui seed to camelCase * update otherVersions type * change otherVersions: null to 'none' * refactor and complete feature * improve static endpoints * fix install params * mask systemd-networkd-wait-online * fix static file fetching * include non-matching versions in otherVersions * convert release notes to modal and clean up displayExver * alert for no other versions * Fix ack-instructions casing * fix indeterminate loader on service install --------- Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: Shadowy Super Coder <musashidisciple@proton.me> Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: J H <dragondef@gmail.com> Co-authored-by: Matt Hill <mattnine@protonmail.com>
444 lines
12 KiB
TypeScript
444 lines
12 KiB
TypeScript
import * as P from "./exver"
|
|
|
|
// prettier-ignore
|
|
export type ValidateVersion<T extends String> =
|
|
T extends `-${infer A}` ? never :
|
|
T extends `${infer A}-${infer B}` ? ValidateVersion<A> & ValidateVersion<B> :
|
|
T extends `${bigint}` ? unknown :
|
|
T extends `${bigint}.${infer A}` ? ValidateVersion<A> :
|
|
never
|
|
|
|
// prettier-ignore
|
|
export type ValidateExVer<T extends string> =
|
|
T extends `#${string}:${infer A}:${infer B}` ? ValidateVersion<A> & ValidateVersion<B> :
|
|
T extends `${infer A}:${infer B}` ? ValidateVersion<A> & ValidateVersion<B> :
|
|
never
|
|
|
|
// prettier-ignore
|
|
export type ValidateExVers<T> =
|
|
T extends [] ? unknown :
|
|
T extends [infer A, ...infer B] ? ValidateExVer<A & string> & ValidateExVers<B> :
|
|
never
|
|
|
|
type Anchor = {
|
|
type: "Anchor"
|
|
operator: P.CmpOp
|
|
version: ExtendedVersion
|
|
}
|
|
|
|
type And = {
|
|
type: "And"
|
|
left: VersionRange
|
|
right: VersionRange
|
|
}
|
|
|
|
type Or = {
|
|
type: "Or"
|
|
left: VersionRange
|
|
right: VersionRange
|
|
}
|
|
|
|
type Not = {
|
|
type: "Not"
|
|
value: VersionRange
|
|
}
|
|
|
|
export class VersionRange {
|
|
private constructor(private atom: Anchor | And | Or | Not | P.Any | P.None) {}
|
|
|
|
toString(): string {
|
|
switch (this.atom.type) {
|
|
case "Anchor":
|
|
return `${this.atom.operator}${this.atom.version}`
|
|
case "And":
|
|
return `(${this.atom.left.toString()}) && (${this.atom.right.toString()})`
|
|
case "Or":
|
|
return `(${this.atom.left.toString()}) || (${this.atom.right.toString()})`
|
|
case "Not":
|
|
return `!(${this.atom.value.toString()})`
|
|
case "Any":
|
|
return "*"
|
|
case "None":
|
|
return "!"
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a boolean indicating whether a given version satisfies the VersionRange
|
|
* !( >= 1:1 <= 2:2) || <=#bitcoin:1.2.0-alpha:0
|
|
*/
|
|
satisfiedBy(version: ExtendedVersion): boolean {
|
|
switch (this.atom.type) {
|
|
case "Anchor":
|
|
const otherVersion = this.atom.version
|
|
switch (this.atom.operator) {
|
|
case "=":
|
|
return version.equals(otherVersion)
|
|
case ">":
|
|
return version.greaterThan(otherVersion)
|
|
case "<":
|
|
return version.lessThan(otherVersion)
|
|
case ">=":
|
|
return version.greaterThanOrEqual(otherVersion)
|
|
case "<=":
|
|
return version.lessThanOrEqual(otherVersion)
|
|
case "!=":
|
|
return !version.equals(otherVersion)
|
|
case "^":
|
|
const nextMajor = this.atom.version.incrementMajor()
|
|
if (
|
|
version.greaterThanOrEqual(otherVersion) &&
|
|
version.lessThan(nextMajor)
|
|
) {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
case "~":
|
|
const nextMinor = this.atom.version.incrementMinor()
|
|
if (
|
|
version.greaterThanOrEqual(otherVersion) &&
|
|
version.lessThan(nextMinor)
|
|
) {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
case "And":
|
|
return (
|
|
this.atom.left.satisfiedBy(version) &&
|
|
this.atom.right.satisfiedBy(version)
|
|
)
|
|
case "Or":
|
|
return (
|
|
this.atom.left.satisfiedBy(version) ||
|
|
this.atom.right.satisfiedBy(version)
|
|
)
|
|
case "Not":
|
|
return !this.atom.value.satisfiedBy(version)
|
|
case "Any":
|
|
return true
|
|
case "None":
|
|
return false
|
|
}
|
|
}
|
|
|
|
private static parseAtom(atom: P.VersionRangeAtom): VersionRange {
|
|
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,
|
|
),
|
|
),
|
|
})
|
|
default:
|
|
return new VersionRange(atom)
|
|
}
|
|
}
|
|
|
|
private static parseRange(range: P.VersionRange): VersionRange {
|
|
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
|
|
}
|
|
|
|
static parse(range: string): VersionRange {
|
|
return VersionRange.parseRange(
|
|
P.parse(range, { startRule: "VersionRange" }),
|
|
)
|
|
}
|
|
|
|
and(right: VersionRange) {
|
|
return new VersionRange({ type: "And", left: this, right })
|
|
}
|
|
|
|
or(right: VersionRange) {
|
|
return new VersionRange({ type: "Or", left: this, right })
|
|
}
|
|
|
|
not() {
|
|
return new VersionRange({ type: "Not", value: this })
|
|
}
|
|
|
|
static anchor(operator: P.CmpOp, version: ExtendedVersion) {
|
|
return new VersionRange({ type: "Anchor", operator, version })
|
|
}
|
|
|
|
static any() {
|
|
return new VersionRange({ type: "Any" })
|
|
}
|
|
|
|
static none() {
|
|
return new VersionRange({ type: "None" })
|
|
}
|
|
}
|
|
|
|
export class Version {
|
|
constructor(
|
|
public number: number[],
|
|
public prerelease: (string | number)[],
|
|
) {}
|
|
|
|
toString(): string {
|
|
return `${this.number.join(".")}${this.prerelease.length > 0 ? `-${this.prerelease.join(".")}` : ""}`
|
|
}
|
|
|
|
compare(other: Version): "greater" | "equal" | "less" {
|
|
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.number.length, other.number.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"
|
|
}
|
|
|
|
static parse(version: string): Version {
|
|
const parsed = P.parse(version, { startRule: "Version" })
|
|
return new Version(parsed.number, parsed.prerelease)
|
|
}
|
|
}
|
|
|
|
// #flavor:0.1.2-beta.1:0
|
|
export class ExtendedVersion {
|
|
constructor(
|
|
public flavor: string | null,
|
|
public upstream: Version,
|
|
public downstream: Version,
|
|
) {}
|
|
|
|
toString(): string {
|
|
return `${this.flavor ? `#${this.flavor}:` : ""}${this.upstream.toString()}:${this.downstream.toString()}`
|
|
}
|
|
|
|
compare(other: ExtendedVersion): "greater" | "equal" | "less" | null {
|
|
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)
|
|
}
|
|
|
|
compareLexicographic(other: ExtendedVersion): "greater" | "equal" | "less" {
|
|
if ((this.flavor || "") > (other.flavor || "")) {
|
|
return "greater"
|
|
} else if ((this.flavor || "") > (other.flavor || "")) {
|
|
return "less"
|
|
} else {
|
|
return this.compare(other)!
|
|
}
|
|
}
|
|
|
|
compareForSort(other: ExtendedVersion): 1 | 0 | -1 {
|
|
switch (this.compareLexicographic(other)) {
|
|
case "greater":
|
|
return 1
|
|
case "equal":
|
|
return 0
|
|
case "less":
|
|
return -1
|
|
}
|
|
}
|
|
|
|
greaterThan(other: ExtendedVersion): boolean {
|
|
return this.compare(other) === "greater"
|
|
}
|
|
|
|
greaterThanOrEqual(other: ExtendedVersion): boolean {
|
|
return ["greater", "equal"].includes(this.compare(other) as string)
|
|
}
|
|
|
|
equals(other: ExtendedVersion): boolean {
|
|
return this.compare(other) === "equal"
|
|
}
|
|
|
|
lessThan(other: ExtendedVersion): boolean {
|
|
return this.compare(other) === "less"
|
|
}
|
|
|
|
lessThanOrEqual(other: ExtendedVersion): boolean {
|
|
return ["less", "equal"].includes(this.compare(other) as string)
|
|
}
|
|
|
|
static parse(extendedVersion: string): ExtendedVersion {
|
|
const parsed = P.parse(extendedVersion, { startRule: "ExtendedVersion" })
|
|
return new ExtendedVersion(
|
|
parsed.flavor,
|
|
new Version(parsed.upstream.number, parsed.upstream.prerelease),
|
|
new Version(parsed.downstream.number, parsed.downstream.prerelease),
|
|
)
|
|
}
|
|
|
|
static parseEmver(extendedVersion: string): ExtendedVersion {
|
|
const parsed = P.parse(extendedVersion, { startRule: "EmVer" })
|
|
return new ExtendedVersion(
|
|
parsed.flavor,
|
|
new Version(parsed.upstream.number, parsed.upstream.prerelease),
|
|
new Version(parsed.downstream.number, parsed.downstream.prerelease),
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 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(): ExtendedVersion {
|
|
const majorIdx = this.upstream.number.findIndex((num: number) => num !== 0)
|
|
|
|
const majorNumber = this.upstream.number.map((num, idx): number => {
|
|
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(): ExtendedVersion {
|
|
const majorIdx = this.upstream.number.findIndex((num: number) => num !== 0)
|
|
let minorIdx = majorIdx === -1 ? majorIdx : majorIdx + 1
|
|
|
|
const majorNumber = this.upstream.number.map((num, idx): number => {
|
|
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,
|
|
)
|
|
}
|
|
}
|
|
|
|
export const testTypeExVer = <T extends string>(t: T & ValidateExVer<T>) => t
|
|
|
|
export const testTypeVersion = <T extends string>(t: T & ValidateVersion<T>) =>
|
|
t
|
|
function tests() {
|
|
testTypeVersion("1.2.3")
|
|
testTypeVersion("1")
|
|
testTypeVersion("12.34.56")
|
|
testTypeVersion("1.2-3")
|
|
testTypeVersion("1-3")
|
|
// @ts-expect-error
|
|
testTypeVersion("-3")
|
|
// @ts-expect-error
|
|
testTypeVersion("1.2.3:1")
|
|
// @ts-expect-error
|
|
testTypeVersion("#cat:1:1")
|
|
|
|
testTypeExVer("1.2.3:1.2.3")
|
|
testTypeExVer("1.2.3.4.5.6.7.8.9.0:1")
|
|
testTypeExVer("100:1")
|
|
testTypeExVer("#cat:1:1")
|
|
testTypeExVer("1.2.3.4.5.6.7.8.9.11.22.33:1")
|
|
testTypeExVer("1-0:1")
|
|
testTypeExVer("1-0:1")
|
|
// @ts-expect-error
|
|
testTypeExVer("1.2-3")
|
|
// @ts-expect-error
|
|
testTypeExVer("1-3")
|
|
// @ts-expect-error
|
|
testTypeExVer("1.2.3.4.5.6.7.8.9.0.10:1" as string)
|
|
// @ts-expect-error
|
|
testTypeExVer("1.-2:1")
|
|
// @ts-expect-error
|
|
testTypeExVer("1..2.3:3")
|
|
}
|