Files
start-os/sdk/lib/exver/index.ts
Lucy a535fc17c3 Feature/fe new registry (#2647)
* 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>
2024-07-23 00:48:12 +00:00

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")
}