libtea semvers (#639)

This commit is contained in:
ABevier 2023-06-01 11:14:40 -04:00 committed by GitHub
parent aa680c8b41
commit 76e86d64d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 16 additions and 492 deletions

View file

@ -4,7 +4,7 @@ import { authFileState } from "./auth";
import * as https from "https";
import { spawn } from "child_process";
import path from "path";
import { parse as semverParse } from "@tea/libtea";
import { semver } from "@teaxyz/lib";
type InitState = "NOT_INITIALIZED" | "PENDING" | "INITIALIZED";
@ -72,7 +72,7 @@ export const cliInitializationState = new InitWatcher<string>(async () => {
return installTeaCli();
} else {
const dir = fs.readlinkSync(teaCliPrefix);
const v = semverParse(dir)?.toString();
const v = semver.parse(dir)?.toString();
if (!v) throw new Error(`couldn't parse to semver: ${dir}`);
return v;
}

View file

@ -5,7 +5,7 @@ import log from "./logger";
import type { InstalledPackage } from "../../src/libs/types";
import { mkdirp } from "mkdirp";
import fetch from "node-fetch";
import { SemVer, isValidSemVer } from "@tea/libtea";
import { SemVer, semver } from "@teaxyz/lib";
import { execSync } from "child_process";
import chokidar from "chokidar";
import { MainWindowNotifier } from "./types";
@ -53,8 +53,8 @@ async function findInstalledVersions(pkgsPath: string): Promise<InstalledPackage
log.info("recursively reading:", pkgsPath);
const folders = await deepReadDir({
dir: pkgsPath,
continueDeeper: (name: string) => !isValidSemVer(name) && name !== ".tea",
filter: (name: string) => !!isValidSemVer(name) && name !== ".tea"
continueDeeper: (name: string) => !semver.isValid(name) && name !== ".tea",
filter: (name: string) => semver.isValid(name) && name !== ".tea"
});
const bottles = folders
@ -218,7 +218,7 @@ export async function startMonitoringTeaDir(mainWindowNotifier: MainWindowNotifi
.on("addDir", (pth) => {
const dir = path.dirname(pth);
const version = path.basename(pth);
if (isValidSemVer(version) && !fs.lstatSync(pth).isSymbolicLink()) {
if (semver.isValid(version) && !fs.lstatSync(pth).isSymbolicLink()) {
const full_name = dir.split(".tea/")[1];
log.info(`Monitor - Added Package: ${full_name} v${version}`);
mainWindowNotifier("pkg-modified", { full_name, version, type: "add" });
@ -228,7 +228,7 @@ export async function startMonitoringTeaDir(mainWindowNotifier: MainWindowNotifi
// FIXME: unlinkDir does not always fire, this is a bug in chokidar
const dir = path.dirname(pth);
const version = path.basename(pth);
if (isValidSemVer(version)) {
if (semver.isValid(version)) {
const full_name = dir.split(".tea/")[1];
log.info(`Monitor - Removed Package: ${full_name} v${version}`);
mainWindowNotifier("pkg-modified", { full_name, version, type: "remove" });

View file

@ -39,7 +39,6 @@
"@sveltejs/adapter-node": "^1.0.0-next.101",
"@sveltejs/adapter-static": "^1.0.0-next.48",
"@sveltejs/kit": "^1.15.9",
"@tea/libtea": "workspace:*",
"@tea/ui": "workspace:*",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/svelte": "^3.2.2",
@ -89,7 +88,7 @@
"@sentry/electron": "^4.4.0",
"@sentry/svelte": "^7.47.0",
"@sentry/vite-plugin": "^0.7.2",
"@teaxyz/lib": "^0.1.9",
"@teaxyz/lib": "^0.2.2",
"@types/electron": "^1.6.10",
"@types/mousetrap": "^1.6.11",
"@vitest/coverage-c8": "^0.27.1",
@ -126,8 +125,7 @@
},
"pnpm": {
"onlyBuiltDependencies": [
"@tea/ui",
"@tea/libtea"
"@tea/ui"
]
},
"homepage": "https://tea.xyz",

View file

@ -1,6 +1,6 @@
import log from "$libs/logger";
import type { GUIPackage } from "$libs/types";
import { SemVer } from "@tea/libtea";
import SemVer from "@teaxyz/lib/semver";
// Find a list of available versions for a package based on the bottles
export const findAvailableVersions = (pkg: Pick<GUIPackage, "bottles" | "version">) => {

View file

@ -18,7 +18,8 @@
"@native": ["./src/libs/native-electron.ts"],
"$components/*": ["./src/components/*"],
"@tea/ui/*": ["../ui/src/*"],
"@tea/libtea/*": ["../libtea/src/*"]
// FIXME: Ideally we don't have to do this, but something is wrong with module resolution
"@teaxyz/lib/semver": ["./../../node_modules/@teaxyz/lib/esm/src/utils/semver.d.ts"]
},
"typeRoots": ["./node_modules/@types", "./types"]
},

View file

@ -26,7 +26,6 @@ const config: UserConfig = {
resolve: {
alias: {
"@tea/ui/*": path.resolve("../ui/src/*"),
"@tea/libtea/*": path.resolve("../libtea/src/*"),
// this dynamic-ish static importing is followed by the svelte build
// but for vscode editing intellisense tsconfig.json is being used
"@native": isMock

View file

@ -1,17 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
coverage/*
build/*
dist/*
electron/dist/*

View file

@ -1,22 +0,0 @@
module.exports = {
root: true,
globals: {
NodeJS: true
},
parser: "@typescript-eslint/parser",
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
plugins: ["@typescript-eslint"],
ignorePatterns: ["*.cjs"],
parserOptions: {
sourceType: "module",
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
},
rules: {
"@typescript-eslint/ban-ts-comment": ["error", { "ts-ignore": "allow-with-description" }]
}
};

View file

@ -1,2 +0,0 @@
node_modules
dist/*

View file

@ -1 +0,0 @@
dist/*

View file

@ -1,10 +0,0 @@
{
"tabWidth": 2,
"useTabs": false,
"singleQuote": false,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"pluginSearchDirs": ["../../node_modules"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View file

@ -1,17 +0,0 @@
{
"name": "@tea/libtea",
"version": "0.0.1",
"description": "library of tea utilities",
"author": "tea.xyz",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"devDependencies": {
"typescript": "^4.7.4"
},
"scripts": {
"build": "tsc --build",
"postinstall": "tsc --build",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
}
}

View file

@ -1 +0,0 @@
export { default as SemVer, isValidSemVer, parse } from "./semver";

View file

@ -1,376 +0,0 @@
//
// This is a copy from tea/cli 0.31.2
// https://github.com/teaxyz/cli/blob/31c2fc0bbddc41cbd63d9ee14593efe3f9b09301/src/utils/semver.ts
//
/**
* we have our own implementation because open source is full of weird
* but *almost* valid semver schemes, eg:
* openssl 1.1.1q
* ghc 5.64.3.2
* it also allows us to implement semver_intersection without hating our lives
*/
export default class SemVer {
readonly components: number[];
major: number;
minor: number;
patch: number;
//FIXME parse these
readonly prerelease: string[] = [];
readonly build: string[] = [];
readonly raw: string;
readonly pretty?: string;
constructor(
input: string | number[] | Range | SemVer,
{ tolerant }: { tolerant: boolean } = { tolerant: false }
) {
if (typeof input == "string") {
const vprefix = input.startsWith("v");
const raw = vprefix ? input.slice(1) : input;
const parts = raw.split(".");
if (parts.length < 3 && !vprefix && !tolerant)
throw new Error(`too short to parse without a \`v\` prefix: ${input}`);
let pretty_is_raw = false;
this.components = parts.flatMap((x, index) => {
const match = x.match(/^(\d+)([a-z])$/);
if (match) {
if (index != parts.length - 1) throw new Error(`invalid version: ${input}`);
const n = parseInt(match[1]);
if (isNaN(n)) throw new Error(`invalid version: ${input}`);
pretty_is_raw = true;
return [n, char_to_num(match[2])];
} else if (/^\d+$/.test(x)) {
const n = parseInt(x); // parseInt will parse eg. `5-start` to `5`
if (isNaN(n)) throw new Error(`invalid version: ${input}`);
return [n];
} else {
throw new Error(`invalid version: ${input}`);
}
});
this.raw = raw;
if (pretty_is_raw) this.pretty = raw;
} else if (input instanceof Range || input instanceof SemVer) {
const v = input instanceof Range ? input.single() : input;
if (!v) throw new Error(`range represents more than a single version: ${input}`);
this.components = v.components;
this.raw = v.raw;
this.pretty = v.pretty;
} else {
this.components = input;
this.raw = input.join(".");
}
this.major = this.components[0];
this.minor = this.components[1] ?? 0;
this.patch = this.components[2] ?? 0;
function char_to_num(c: string) {
return c.charCodeAt(0) - "a".charCodeAt(0) + 1;
}
}
toString(): string {
return (
this.pretty ??
(this.components.length <= 3
? `${this.major}.${this.minor}.${this.patch}`
: this.components.join("."))
);
}
eq(that: SemVer): boolean {
return this.compare(that) == 0;
}
neq(that: SemVer): boolean {
return this.compare(that) != 0;
}
gt(that: SemVer): boolean {
return this.compare(that) > 0;
}
lt(that: SemVer): boolean {
return this.compare(that) < 0;
}
compare(that: SemVer): number {
return _compare(this, that);
}
[Symbol.for("Deno.customInspect")]() {
return this.toString();
}
}
/// the same as the constructor but swallows the error returning undefined instead
/// also slightly more tolerant parsing
export function parse(input: string, tolerant = true) {
try {
return new SemVer(input, { tolerant });
} catch {
return undefined;
}
}
export function isValidSemVer(input: string, tolerant = false) {
return parse(input, tolerant) != undefined;
}
/// we dont support as much as node-semver but we refuse to do so because it is badness
export class Range {
// contract [0, 1] where 0 != 1 and 0 < 1
readonly set: ([SemVer, SemVer] | SemVer)[] | "*";
constructor(input: string | ([SemVer, SemVer] | SemVer)[]) {
if (input === "*") {
this.set = "*";
} else if (!isString(input)) {
this.set = input;
} else {
input = input.trim();
const err = () => new Error(`invalid semver range: ${input}`);
this.set = input.split(/(?:,|\s*\|\|\s*)/).map((input) => {
let match = input.match(/^>=((\d+\.)*\d+)\s*(<((\d+\.)*\d+))?$/);
if (match) {
const v1 = new SemVer(match[1], { tolerant: true });
const v2 = match[3]
? new SemVer(match[4], { tolerant: true })
: new SemVer([Infinity, Infinity, Infinity]);
return [v1, v2];
} else if ((match = input.match(/^([~=<^])(.+)$/))) {
let v1: SemVer | undefined, v2: SemVer | undefined;
switch (match[1]) {
case "^":
v1 = new SemVer(match[2], { tolerant: true });
// eslint-disable-next-line no-case-declarations
const parts: number[] = [];
for (let i = 0; i < v1.components.length; i++) {
if (v1.components[i] === 0 && i < v1.components.length - 1) {
parts.push(0);
} else {
parts.push(v1.components[i] + 1);
break;
}
}
v2 = new SemVer(parts, { tolerant: true });
return [v1, v2];
case "~":
{
v1 = new SemVer(match[2], { tolerant: true });
if (v1.components.length == 1) {
// yep this is the official policy
v2 = new SemVer([v1.major + 1], { tolerant: true });
} else {
v2 = new SemVer([v1.major, v1.minor + 1], { tolerant: true });
}
}
return [v1, v2];
case "<":
v1 = new SemVer([0], { tolerant: true });
v2 = new SemVer(match[2], { tolerant: true });
return [v1, v2];
case "=":
return new SemVer(match[2], { tolerant: true });
}
}
throw err();
});
if (this.set.length == 0) {
throw err();
}
for (const i of this.set) {
if (isArray(i) && !i[0].lt(i[1])) throw err();
}
}
}
toString(): string {
if (this.set === "*") {
return "*";
} else {
return this.set
.map((v) => {
if (!isArray(v)) return `=${v.toString()}`;
const [v1, v2] = v;
if (v1.major > 0 && v2.major == v1.major + 1 && v2.minor == 0 && v2.patch == 0) {
const v = chomp(v1);
return `^${v}`;
} else if (v2.major == v1.major && v2.minor == v1.minor + 1 && v2.patch == 0) {
const v = chomp(v1);
return `~${v}`;
} else if (v2.major == Infinity) {
const v = chomp(v1);
return `>=${v}`;
} else {
return `>=${chomp(v1)}<${chomp(v2)}`;
}
})
.join(",");
}
}
// eq(that: Range): boolean {
// if (this.set.length !== that.set.length) return false
// for (let i = 0; i < this.set.length; i++) {
// const [a,b] = [this.set[i], that.set[i]]
// if (typeof a !== 'string' && typeof b !== 'string') {
// if (a[0].neq(b[0])) return false
// if (a[1].neq(b[1])) return false
// } else if (a != b) {
// return false
// }
// }
// return true
// }
/// tolerant to stuff in the wild that hasnt semver specifiers
static parse(input: string): Range | undefined {
try {
return new Range(input);
} catch {
if (!/^(\d+\.)*\d+$/.test(input)) return;
// AFAICT this is what people expect
// verified via https://jubianchi.github.io/semver-check/
const parts = input.split(".");
if (parts.length < 3) {
return new Range(`^${input}`);
} else {
return new Range(`~${input}`);
}
}
}
satisfies(version: SemVer): boolean {
if (this.set === "*") {
return true;
} else {
return this.set.some((v) => {
if (isArray(v)) {
const [v1, v2] = v;
return version.compare(v1) >= 0 && version.compare(v2) < 0;
} else {
return version.eq(v);
}
});
}
}
max(versions: SemVer[]): SemVer | undefined {
return versions
.filter((x) => this.satisfies(x))
.sort((a, b) => a.compare(b))
.pop();
}
single(): SemVer | undefined {
if (this.set === "*") return;
if (this.set.length > 1) return;
return isArray(this.set[0]) ? undefined : this.set[0];
}
[Symbol.for("Deno.customInspect")]() {
return this.toString();
}
}
function zip<T, U>(a: T[], b: U[]) {
const N = Math.max(a.length, b.length);
const rv: [T | undefined, U | undefined][] = [];
for (let i = 0; i < N; ++i) {
rv.push([a[i], b[i]]);
}
return rv;
}
function _compare(a: SemVer, b: SemVer): number {
for (const [c, d] of zip(a.components, b.components)) {
if (c != d) return (c ?? 0) - (d ?? 0);
}
return 0;
}
export { _compare as compare };
export function intersect(a: Range, b: Range): Range {
if (b.set === "*") return a;
if (a.set === "*") return b;
// calculate the intersection between two semver.Ranges
const set: ([SemVer, SemVer] | SemVer)[] = [];
for (const aa of a.set) {
for (const bb of b.set) {
if (!isArray(aa) && !isArray(bb)) {
if (aa.eq(bb)) set.push(aa);
} else if (!isArray(aa)) {
const bbb = bb as [SemVer, SemVer];
if (aa.compare(bbb[0]) >= 0 && aa.lt(bbb[1])) set.push(aa);
} else if (!isArray(bb)) {
const aaa = aa as [SemVer, SemVer];
if (bb.compare(aaa[0]) >= 0 && bb.lt(aaa[1])) set.push(bb);
} else {
const a1 = aa[0];
const a2 = aa[1];
const b1 = bb[0];
const b2 = bb[1];
if (a1.compare(b2) >= 0 || b1.compare(a2) >= 0) {
continue;
}
set.push([a1.compare(b1) > 0 ? a1 : b1, a2.compare(b2) < 0 ? a2 : b2]);
}
}
}
if (set.length <= 0) throw new Error(`cannot intersect: ${a} && ${b}`);
return new Range(set);
}
//FIXME yes yes this is not sufficient
export const regex = /\d+\.\d+\.\d+/;
function chomp(v: SemVer) {
return v.toString().replace(/(\.0)+$/g, "") || "0";
}
/**
* Returns whether the payload is an array
*
* @param {any} payload
* @returns {payload is any[]}
*/
export function isArray(payload: unknown): payload is unknown[] {
return getType(payload) === "Array";
}
/**
* Returns whether the payload is a string
*
* @param {*} payload
* @returns {payload is string}
*/
export function isString(payload: unknown): payload is string {
return getType(payload) === "String";
}
/**
* Returns the object type of the given payload
*
* @param {*} payload
* @returns {string}
*/
export function getType(payload: unknown): string {
return Object.prototype.toString.call(payload).slice(8, -1);
}

View file

@ -1,20 +0,0 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"declaration": true,
"module": "ES6",
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"importHelpers": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"lib": ["es2019"],
"outDir": "./dist"
},
"include": ["./src/**/*.ts"]
}

View file

@ -29,9 +29,8 @@ importers:
'@sveltejs/adapter-node': ^1.0.0-next.101
'@sveltejs/adapter-static': ^1.0.0-next.48
'@sveltejs/kit': ^1.15.9
'@tea/libtea': workspace:*
'@tea/ui': workspace:*
'@teaxyz/lib': ^0.1.9
'@teaxyz/lib': ^0.2.2
'@testing-library/jest-dom': ^5.16.5
'@testing-library/svelte': ^3.2.2
'@testing-library/webdriverio': ^3.2.1
@ -111,7 +110,7 @@ importers:
'@sentry/electron': 4.5.0
'@sentry/svelte': 7.51.2_svelte@3.59.1
'@sentry/vite-plugin': 0.7.2
'@teaxyz/lib': 0.1.9
'@teaxyz/lib': 0.2.2
'@types/electron': 1.6.10
'@types/mousetrap': 1.6.11
'@vitest/coverage-c8': 0.27.3_jsdom@21.1.2
@ -153,7 +152,6 @@ importers:
'@sveltejs/adapter-node': 1.2.4_@sveltejs+kit@1.16.2
'@sveltejs/adapter-static': 1.0.6_@sveltejs+kit@1.16.2
'@sveltejs/kit': 1.16.2_svelte@3.59.1+vite@4.3.5
'@tea/libtea': link:../libtea
'@tea/ui': link:../ui
'@testing-library/jest-dom': 5.16.5
'@testing-library/svelte': 3.2.2_svelte@3.59.1
@ -195,12 +193,6 @@ importers:
vitest: 0.28.5_jsdom@21.1.2
wdio-electron-service: 4.0.2_qkoyiluaudwk5orvhjdvg63jhu
modules/libtea:
specifiers:
typescript: ^4.7.4
devDependencies:
typescript: 4.9.5
modules/ui:
specifiers:
'@magidoc/plugin-svelte-prismjs': ^3.0.6
@ -3645,8 +3637,8 @@ packages:
tailwindcss: 3.3.2
dev: false
/@teaxyz/lib/0.1.9:
resolution: {integrity: sha512-LukZXiLo22bq5P81O4rvUPLpqvTt+aNwn73tUN3Fd2QLtPfsh70qBy/uHM1B5y0+IDfWjn/mvCXra6ZARA+VRA==}
/@teaxyz/lib/0.2.2:
resolution: {integrity: sha512-xChLHuuwbUXZcHMmqMsGIB6RbXqEb1hXgRYJk4Zg3KfozhRllXuiZ6VkUZYPib66ygMjPlndclG7tj2cDlsLkg==}
dependencies:
'@deno/shim-crypto': 0.3.1
'@deno/shim-deno': 0.16.1