mirror of
https://github.com/ivabus/gui
synced 2025-04-23 14:07:14 +03:00
show version correctly (#544)
* show version correctly * fix lint/prettier * button style
This commit is contained in:
parent
6c8bc7585f
commit
1d282ac7e4
23 changed files with 572 additions and 82 deletions
|
@ -1,13 +1,11 @@
|
|||
import fs from "fs";
|
||||
import { getGuiPath } from "./tea-dir";
|
||||
import log from "./logger";
|
||||
import semver from "semver";
|
||||
import { cliBinPath, asyncExec } from "./cli";
|
||||
import { createInitialSessionFile } from "./auth";
|
||||
import semverCompare from "semver/functions/compare";
|
||||
import { SemVer, isValidSemVer } from "@tea/libtea";
|
||||
|
||||
// Versions before this do not support the --json flag
|
||||
const MINIMUM_TEA_VERSION = "0.28.3";
|
||||
const MINIMUM_TEA_VERSION = "0.31.2";
|
||||
|
||||
const destinationDirectory = getGuiPath();
|
||||
|
||||
|
@ -35,55 +33,46 @@ export async function initializeTeaCli(): Promise<string> {
|
|||
}
|
||||
|
||||
async function initializeTeaCliInternal(): Promise<string> {
|
||||
try {
|
||||
let binCheck = "";
|
||||
let needsUpdate = false;
|
||||
let binCheck = "";
|
||||
let needsUpdate = false;
|
||||
|
||||
// Create the destination directory if it doesn't exist
|
||||
if (!fs.existsSync(destinationDirectory)) {
|
||||
fs.mkdirSync(destinationDirectory, { recursive: true });
|
||||
}
|
||||
|
||||
// replace this with max's pr
|
||||
const curlCommand = `curl --insecure -L -o "${cliBinPath}" "${binaryUrl}"`;
|
||||
|
||||
const exists = fs.existsSync(cliBinPath);
|
||||
if (exists) {
|
||||
log.info("binary tea already exists at", cliBinPath);
|
||||
try {
|
||||
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
|
||||
const teaVersion = binCheck.toString().split(" ")[1];
|
||||
if (semverCompare(teaVersion, MINIMUM_TEA_VERSION) < 0) {
|
||||
log.info("binary tea version is too old, updating");
|
||||
needsUpdate = true;
|
||||
}
|
||||
} catch (error) {
|
||||
// probably binary is not executable or no permission
|
||||
log.error("Error checking tea binary version:", error);
|
||||
needsUpdate = true;
|
||||
await asyncExec(`cd ${destinationDirectory} && rm tea`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists || needsUpdate) {
|
||||
try {
|
||||
await asyncExec(curlCommand);
|
||||
log.info("Binary downloaded and saved to", cliBinPath);
|
||||
await asyncExec("chmod u+x " + cliBinPath);
|
||||
log.info("Binary is now ready for use at", cliBinPath);
|
||||
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
|
||||
} catch (error) {
|
||||
log.error("Error setting-up tea binary:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const version = binCheck.toString().split(" ")[1];
|
||||
log.info("binary tea version:", version);
|
||||
return semver.valid(version.trim()) ? version : "";
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
return "";
|
||||
// Create the destination directory if it doesn't exist
|
||||
if (!fs.existsSync(destinationDirectory)) {
|
||||
fs.mkdirSync(destinationDirectory, { recursive: true });
|
||||
}
|
||||
|
||||
// replace this with max's pr
|
||||
const curlCommand = `curl --insecure -L -o "${cliBinPath}" "${binaryUrl}"`;
|
||||
|
||||
const exists = fs.existsSync(cliBinPath);
|
||||
if (exists) {
|
||||
log.info("binary tea already exists at", cliBinPath);
|
||||
try {
|
||||
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
|
||||
const teaVersion = binCheck.toString().split(" ")[1].trim();
|
||||
if (new SemVer(teaVersion).compare(new SemVer(MINIMUM_TEA_VERSION)) < 0) {
|
||||
log.info("binary tea version is too old, updating");
|
||||
needsUpdate = true;
|
||||
}
|
||||
} catch (error) {
|
||||
// probably binary is not executable or no permission
|
||||
log.error("Error checking tea binary version:", error);
|
||||
needsUpdate = true;
|
||||
await asyncExec(`cd ${destinationDirectory} && rm tea`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists || needsUpdate) {
|
||||
await asyncExec(curlCommand);
|
||||
log.info("Binary downloaded and saved to", cliBinPath);
|
||||
await asyncExec("chmod u+x " + cliBinPath);
|
||||
log.info("Binary is now ready for use at", cliBinPath);
|
||||
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
|
||||
}
|
||||
|
||||
const version = binCheck.toString().split(" ")[1];
|
||||
log.info("binary tea version:", version);
|
||||
return isValidSemVer(version.trim()) ? version : "";
|
||||
}
|
||||
|
||||
export default async function initialize(): Promise<string> {
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { app } from "electron";
|
||||
import semver, { SemVer } from "semver";
|
||||
import log from "./logger";
|
||||
import type { InstalledPackage } from "../../src/libs/types";
|
||||
import semverCompare from "semver/functions/compare";
|
||||
import { mkdirp } from "mkdirp";
|
||||
import fetch from "node-fetch";
|
||||
import { SemVer, isValidSemVer } from "@tea/libtea";
|
||||
|
||||
type Dir = {
|
||||
name: string;
|
||||
|
@ -34,26 +33,26 @@ export async function getInstalledPackages(): Promise<InstalledPackage[]> {
|
|||
log.info("recursively reading:", pkgsPath);
|
||||
const folders = await deepReadDir({
|
||||
dir: pkgsPath,
|
||||
continueDeeper: (name: string) => !semver.valid(name) && name !== ".tea",
|
||||
filter: (name: string) => !!semver.valid(name) && name !== ".tea"
|
||||
continueDeeper: (name: string) => !isValidSemVer(name) && name !== ".tea",
|
||||
filter: (name: string) => !!isValidSemVer(name) && name !== ".tea"
|
||||
});
|
||||
|
||||
const bottles = folders
|
||||
.map((p: string) => p.split(".tea/")[1])
|
||||
.map(parseVersionFromPath)
|
||||
.filter((v): v is ParsedVersion => !!v)
|
||||
.sort((a, b) => semverCompare(b.semVer, a.semVer));
|
||||
.sort((a, b) => b.semVer.compare(a.semVer));
|
||||
|
||||
log.info("installed bottles:", bottles.length);
|
||||
|
||||
return bottles.reduce<InstalledPackage[]>((pkgs, bottle) => {
|
||||
const pkg = pkgs.find((v) => v.full_name === bottle.full_name);
|
||||
if (pkg) {
|
||||
pkg.installed_versions.push(bottle.semVer.version);
|
||||
pkg.installed_versions.push(bottle.semVer.toString());
|
||||
} else {
|
||||
pkgs.push({
|
||||
full_name: bottle.full_name,
|
||||
installed_versions: [bottle.semVer.version]
|
||||
installed_versions: [bottle.semVer.toString()]
|
||||
});
|
||||
}
|
||||
return pkgs;
|
||||
|
@ -65,7 +64,7 @@ const parseVersionFromPath = (versionPath: string): ParsedVersion | null => {
|
|||
const path = versionPath.trim().split("/");
|
||||
const version = path.pop();
|
||||
return {
|
||||
semVer: new SemVer(semver.clean(version || "") || ""),
|
||||
semVer: new SemVer(version ?? ""),
|
||||
full_name: path.join("/")
|
||||
};
|
||||
} catch (e) {
|
||||
|
@ -119,9 +118,9 @@ export const deepReadDir = async ({
|
|||
if (f.isDirectory() && deeper) {
|
||||
const nextFiles = await deepReadDir({ dir: nextPath, continueDeeper, filter });
|
||||
arrayOfFiles.push(...nextFiles);
|
||||
} else if (filter && filter(f.name)) {
|
||||
} else if (!f.isSymbolicLink() && filter && filter(f.name)) {
|
||||
arrayOfFiles.push(nextPath);
|
||||
} else if (!filter) {
|
||||
} else if (!f.isSymbolicLink() && !filter) {
|
||||
arrayOfFiles.push(nextPath);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
"@sveltejs/adapter-static": "^1.0.0-next.48",
|
||||
"@sveltejs/kit": "^1.15.9",
|
||||
"@tea/ui": "workspace:*",
|
||||
"@tea/libtea": "workspace:*",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/svelte": "^3.2.2",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
|
@ -103,7 +104,6 @@
|
|||
"mousetrap": "^1.6.5",
|
||||
"pushy-electron": "^1.0.11",
|
||||
"renderer": "link:@types/electron/renderer",
|
||||
"semver": "^7.3.8",
|
||||
"svelte-infinite-scroll": "^2.0.1",
|
||||
"svelte-markdown": "^0.2.3",
|
||||
"svelte-watch-resize": "^1.0.3",
|
||||
|
@ -114,7 +114,8 @@
|
|||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@tea/ui"
|
||||
"@tea/ui",
|
||||
"@tea/libtea"
|
||||
]
|
||||
},
|
||||
"homepage": "https://tea.xyz",
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import Button from "@tea/ui/button/button.svelte";
|
||||
import ButtonIcon from "@tea/ui/button-icon/button-icon.svelte";
|
||||
import ToolTip from "@tea/ui/tool-tip/tool-tip.svelte";
|
||||
import semverCompare from "semver/functions/compare";
|
||||
import ProgressCircle from "@tea/ui/progress-circle/progress-circle.svelte";
|
||||
|
||||
import type { GUIPackage } from "$libs/types";
|
||||
|
@ -16,6 +15,7 @@
|
|||
import PackageImage from "../package-card/bg-image.svelte";
|
||||
import PackageVersionSelector from "$components/package-install-button/package-version-selector.svelte";
|
||||
import { fixPackageName } from "$libs/packages/pkg-utils";
|
||||
import { semverCompare } from "$libs/packages/pkg-utils";
|
||||
|
||||
export let pkg: GUIPackage;
|
||||
let installing = false;
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
<div class="flex w-fit flex-col items-center">
|
||||
<div class="install-button {layout}" on:mousedown={preventPropagation}>
|
||||
{#if pkg.state === PackageStates.INSTALLED}
|
||||
<PackageInstalledBadge version={pkg.version} />
|
||||
<PackageInstalledBadge {pkg} />
|
||||
{:else}
|
||||
<PackageInstallButton
|
||||
{pkg}
|
||||
|
|
|
@ -24,6 +24,13 @@
|
|||
return state === PackageStates.INSTALLING || state === PackageStates.UPDATING;
|
||||
};
|
||||
|
||||
const getVersion = (pkg: GUIPackage) => {
|
||||
if (pkg.state === PackageStates.INSTALLED) {
|
||||
return pkg.installed_versions?.[0] ?? pkg.version;
|
||||
}
|
||||
return pkg.version;
|
||||
};
|
||||
|
||||
const badgeClass: Record<PackageStates, string> = {
|
||||
[PackageStates.AVAILABLE]: "install-badge",
|
||||
[PackageStates.INSTALLING]: "install-badge",
|
||||
|
@ -53,7 +60,7 @@
|
|||
>
|
||||
<div class="flex items-center gap-x-2">
|
||||
<div>{ctaLabel}</div>
|
||||
<div class="version-label {badgeClass[pkg.state]}">{pkg.version}</div>
|
||||
<div class="version-label {badgeClass[pkg.state]}">v{getVersion(pkg)}</div>
|
||||
</div>
|
||||
{#if hasVersionSelectorDropdown}
|
||||
<i class="icon-downward-arrow flex" />
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
export let version = "1.0.0";
|
||||
import type { GUIPackage } from "$libs/types";
|
||||
|
||||
export let pkg: GUIPackage | null = null;
|
||||
</script>
|
||||
|
||||
<div class="container relative h-full">
|
||||
|
@ -7,7 +9,7 @@
|
|||
<i class="icon-check-circle-o flex text-sm text-[#00ffd0]" />
|
||||
<div class="text-xs">INSTALLED</div>
|
||||
<div class="rounded-sm bg-white px-1 text-[10px] leading-[12px] text-black">
|
||||
v{version}
|
||||
v{pkg?.installed_versions?.[0]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { PackageStates, type GUIPackage } from "$libs/types";
|
||||
import clickOutside from "@tea/ui/lib/clickOutside";
|
||||
import PackageStateButton from "./package-install-button.svelte";
|
||||
import semver from "semver";
|
||||
import { semverCompare } from "$libs/packages/pkg-utils";
|
||||
|
||||
export let buttonSize: "small" | "large" = "small";
|
||||
export let pkg: GUIPackage;
|
||||
|
@ -27,7 +27,7 @@
|
|||
|
||||
$: installedVersions = pkg.installed_versions || [];
|
||||
$: allVersions = Array.from(new Set([pkg.version, ...availableVersions])).sort(
|
||||
(a: string, b: string) => semver.rcompare(a, b)
|
||||
(a: string, b: string) => semverCompare(b, a)
|
||||
);
|
||||
|
||||
const handleClick = (evt: MouseEvent, version: string) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import log from "$libs/logger";
|
||||
import type { GUIPackage } from "$libs/types";
|
||||
import { clean } from "semver";
|
||||
import semverCompare from "semver/functions/compare";
|
||||
import { SemVer } from "@tea/libtea";
|
||||
|
||||
// Find a list of available versions for a package based on the bottles
|
||||
export const findAvailableVersions = (pkg: Pick<GUIPackage, "bottles" | "version">) => {
|
||||
|
@ -15,10 +15,19 @@ export const findAvailableVersions = (pkg: Pick<GUIPackage, "bottles" | "version
|
|||
if (b.arch === arch) versionSet.add(b.version);
|
||||
}
|
||||
|
||||
return Array.from(versionSet).sort((a, b) => semverCompare(cleanVersion(b), cleanVersion(a)));
|
||||
return Array.from(versionSet).sort((a, b) => semverCompare(b, a));
|
||||
};
|
||||
|
||||
export const cleanVersion = (version: string) => clean(version) || "0.0.0";
|
||||
export const semverCompare = (a: string, b: string) => {
|
||||
try {
|
||||
return new SemVer(a).compare(new SemVer(b));
|
||||
} catch (err) {
|
||||
log.error(`Failed to compare versions ${a} and ${b}`, err);
|
||||
// This is bad if it happens, but it's better than crashing, the tea semver library is very permissive
|
||||
// and it would be extremely unlikely for this to happen in practice as how would something get bottled in the first place?
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
};
|
||||
|
||||
// Add a new version to the list of installed versions while maintaining the sort order
|
||||
export const addInstalledVersion = (
|
||||
|
@ -29,9 +38,7 @@ export const addInstalledVersion = (
|
|||
return [newVersion];
|
||||
}
|
||||
|
||||
return [...installedVersions, newVersion].sort((a, b) =>
|
||||
semverCompare(cleanVersion(b), cleanVersion(a))
|
||||
);
|
||||
return [...installedVersions, newVersion].sort((a, b) => semverCompare(b, a));
|
||||
};
|
||||
|
||||
export const findRecentInstalledVersion = (pkg: GUIPackage) => {
|
||||
|
@ -50,3 +57,13 @@ export const isInstalling = (pkg: GUIPackage) => {
|
|||
export const fixPackageName = (title: string) => {
|
||||
return title.replace("-", "\u2011");
|
||||
};
|
||||
|
||||
// Checks if an installed package is up to date. It is assumed that the package is installed.
|
||||
export const isPackageUpToDate = (pkg: GUIPackage) => {
|
||||
if (!pkg.installed_versions?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the installed version is equal or newer than the latest version, it's up to date
|
||||
return semverCompare(pkg.installed_versions[0], pkg.version) >= 0;
|
||||
};
|
||||
|
|
|
@ -27,6 +27,7 @@ import { notificationStore } from "$libs/stores";
|
|||
import withRetry from "$libs/utils/retry";
|
||||
|
||||
import log from "$libs/logger";
|
||||
import { isPackageUpToDate } from "../packages/pkg-utils";
|
||||
|
||||
const packageRefreshInterval = 1000 * 60 * 60; // 1 hour
|
||||
|
||||
|
@ -80,7 +81,7 @@ export default function initPackagesStore() {
|
|||
return PackageStates.AVAILABLE;
|
||||
}
|
||||
|
||||
const isUpToDate = pkg.version === pkg.installed_versions?.[0];
|
||||
const isUpToDate = isPackageUpToDate(pkg);
|
||||
|
||||
if (isInstalling(pkg)) {
|
||||
const hasNoVersions = !pkg.installed_versions?.length;
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
{/if}
|
||||
<div>
|
||||
<Button
|
||||
class="h-8 w-48 text-xs"
|
||||
class="h-8 w-48 p-2 text-xs"
|
||||
loading={updating}
|
||||
type="plain"
|
||||
color="secondary"
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
"$libs/*": ["./src/libs/*"],
|
||||
"@native": ["./src/libs/native-electron.ts"],
|
||||
"$components/*": ["./src/components/*"],
|
||||
"@tea/ui/*": ["../ui/src/*"]
|
||||
"@tea/ui/*": ["../ui/src/*"],
|
||||
"@tea/libtea/*": ["../libtea/src/*"]
|
||||
},
|
||||
"typeRoots": ["./node_modules/@types", "./types"]
|
||||
},
|
||||
|
|
|
@ -9,6 +9,7 @@ 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
|
||||
|
|
17
modules/libtea/.eslintignore
Normal file
17
modules/libtea/.eslintignore
Normal file
|
@ -0,0 +1,17 @@
|
|||
.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/*
|
22
modules/libtea/.eslintrc.cjs
Normal file
22
modules/libtea/.eslintrc.cjs
Normal file
|
@ -0,0 +1,22 @@
|
|||
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" }]
|
||||
}
|
||||
};
|
2
modules/libtea/.gitignore
vendored
Normal file
2
modules/libtea/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
dist/*
|
1
modules/libtea/.prettierignore
Normal file
1
modules/libtea/.prettierignore
Normal file
|
@ -0,0 +1 @@
|
|||
dist/*
|
10
modules/libtea/.prettierrc
Normal file
10
modules/libtea/.prettierrc
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"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" } }]
|
||||
}
|
17
modules/libtea/package.json
Normal file
17
modules/libtea/package.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"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 ."
|
||||
}
|
||||
}
|
1
modules/libtea/src/index.ts
Normal file
1
modules/libtea/src/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default as SemVer, isValidSemVer } from "./semver";
|
376
modules/libtea/src/semver.ts
Normal file
376
modules/libtea/src/semver.ts
Normal file
|
@ -0,0 +1,376 @@
|
|||
//
|
||||
// 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 don’t 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 hasn’t 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);
|
||||
}
|
20
modules/libtea/tsconfig.json
Normal file
20
modules/libtea/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"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"]
|
||||
}
|
|
@ -26,6 +26,7 @@ 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:*
|
||||
'@testing-library/jest-dom': ^5.16.5
|
||||
'@testing-library/svelte': ^3.2.2
|
||||
|
@ -74,7 +75,6 @@ importers:
|
|||
prettier-plugin-tailwindcss: ^0.2.8
|
||||
pushy-electron: ^1.0.11
|
||||
renderer: link:@types/electron/renderer
|
||||
semver: ^7.3.8
|
||||
svelte: ^3.55.1
|
||||
svelte-check: ^2.8.0
|
||||
svelte-infinite-scroll: ^2.0.1
|
||||
|
@ -122,7 +122,6 @@ importers:
|
|||
mousetrap: 1.6.5
|
||||
pushy-electron: 1.0.11
|
||||
renderer: link:@types/electron/renderer
|
||||
semver: 7.5.0
|
||||
svelte-infinite-scroll: 2.0.1
|
||||
svelte-markdown: 0.2.3_svelte@3.58.0
|
||||
svelte-watch-resize: 1.0.3
|
||||
|
@ -138,6 +137,7 @@ importers:
|
|||
'@sveltejs/adapter-node': 1.2.4_@sveltejs+kit@1.15.9
|
||||
'@sveltejs/adapter-static': 1.0.6_@sveltejs+kit@1.15.9
|
||||
'@sveltejs/kit': 1.15.9_svelte@3.58.0+vite@4.3.3
|
||||
'@tea/libtea': link:../libtea
|
||||
'@tea/ui': link:../ui
|
||||
'@testing-library/jest-dom': 5.16.5
|
||||
'@testing-library/svelte': 3.2.2_svelte@3.58.0
|
||||
|
@ -171,6 +171,12 @@ importers:
|
|||
vite: 4.3.3
|
||||
vitest: 0.28.5_jsdom@21.1.2
|
||||
|
||||
modules/libtea:
|
||||
specifiers:
|
||||
typescript: ^4.7.4
|
||||
devDependencies:
|
||||
typescript: 4.9.5
|
||||
|
||||
modules/ui:
|
||||
specifiers:
|
||||
'@magidoc/plugin-svelte-prismjs': ^3.0.6
|
||||
|
|
Loading…
Reference in a new issue