Files
ts-scripts/libraries/deno-commons-mod.ts
2026-01-14 23:53:06 +08:00

536 lines
15 KiB
TypeScript

// Reference:
// - https://docs.deno.com/runtime/fundamentals/testing/
import { assert } from "jsr:@std/assert/assert";
import { assertEquals } from "jsr:@std/assert";
import { decodeBase64, encodeBase64 } from "jsr:@std/encoding/base64";
import { dirname } from "https://deno.land/std@0.208.0/path/mod.ts";
// reference: https://docs.deno.com/examples/hex_base64_encoding/
// import { decodeBase64, encodeBase64 } from "jsr:@std/encoding/base64";
// import { decodeHex, encodeHex } from "jsr:@std/encoding/hex";
export async function sleep(timeoutMillis: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, timeoutMillis));
}
export function compareVersion(ver1: string, ver2: string): 0 | 1 | -1 {
if (ver1 === ver2) return 0;
const ver1Parts = ver1.split(".");
const ver2Parts = ver2.split(".");
const ver1Main = parseInt(ver1Parts[0]);
const ver2Main = parseInt(ver2Parts[0]);
if (ver1Main > ver2Main) return 1;
if (ver1Main < ver2Main) return -1;
const ver1Second = parseInt(ver1Parts[1]);
const ver2Second = parseInt(ver2Parts[1]);
if (ver1Second > ver2Second) return 1;
if (ver1Second < ver2Second) return -1;
const ver1Third = parseInt(ver1Parts[2]);
const ver2Third = parseInt(ver2Parts[2]);
if (ver1Third > ver2Third) return 1;
if (ver1Third < ver2Third) return -1;
return 0;
}
export function isOn(val: string | undefined | null): boolean {
if ((val === null) || (val === undefined)) {
return false;
}
const lowerVal = val.toLowerCase();
return lowerVal === "on" || lowerVal === "yes" || lowerVal === "1" ||
lowerVal === "true";
}
export function getEnv(envKey: string): string | null {
const homeDir = getHomeDir();
if ((homeDir !== null) && envKey) {
const envValue = readFileToStringSync(
`${homeDir}/.config/envs/${envKey}`,
);
if (envValue !== null) {
return envValue.trim();
}
}
return Deno.env.get(envKey) || null;
}
export function isEnvOn(envKey: string): boolean {
return isOn(getEnv(envKey));
}
export function formatHumanTime(timeMillis: number): string {
const times = [];
if (timeMillis < 1000) {
return `${timeMillis}ms`;
}
const timeSecs = Math.floor(timeMillis / 1000);
const timeSecsLow = timeSecs % 60;
if (timeSecsLow > 0) {
times.push(`${timeSecsLow}s`);
}
const timeMinutes = Math.floor(timeSecs / 60);
const timeMinutesLow = timeMinutes % 60;
if (timeMinutesLow > 0) {
times.push(`${timeMinutesLow}m`);
}
const timeHours = Math.floor(timeMinutes / 60);
const timeHoursLow = timeHours % 24;
if (timeHoursLow > 0) {
times.push(`${timeHoursLow}h`);
}
const timeDays = Math.floor(timeHours / 24);
if (timeDays > 0) {
times.push(`${timeDays}d`);
}
return times.reverse().join(" ");
}
export function formatSize(size: number): string {
if (size < 0) {
return "N/A";
}
if (size == 0) {
return "0B";
}
const sizes = [];
const bytesLow = size % 1024;
if (bytesLow > 0) {
sizes.push(`${bytesLow}B`);
}
const kb = Math.floor(size / 1024);
const kbLow = kb % 1024;
if (kbLow > 0) {
sizes.push(`${kbLow}KiB`);
}
const mb = Math.floor(kb / 1024);
const mbLow = mb % 1024;
if (mbLow > 0) {
sizes.push(`${mbLow}MiB`);
}
const gb = Math.floor(mb / 1024);
if (gb > 0) {
sizes.push(`${gb}GiB`);
}
return sizes.reverse().join(" ");
}
export function formatSize2(size: number): string {
if (size < 0) {
return "N/A";
}
if (size < 1024) {
return `${size}B`;
}
if (size < 1024 * 1024) {
return `${formatNumber(size / 1024)}KiB`;
}
return `${formatNumber(size / (1024 * 1024))}MiB`;
}
export function formatPercent(a: number, b: number): string {
if (b == null || b <= 0) {
return "N/A";
}
return formatNumber((a * 100) / b) + "%";
}
export function formatNumber(num: number): string {
const p = num.toString();
const pointIndex = p.indexOf(".");
if (pointIndex < 0) {
return p + ".00";
}
const decimal = p.substring(pointIndex + 1);
const decimalPart = decimal.length == 1
? (decimal + "0")
: decimal.substring(0, 2);
return p.substring(0, pointIndex) + "." + decimalPart;
}
export async function clearLastLine() {
await printLastLine("");
}
export async function printLastLine(line: string) {
await Deno.stdout.write(
new TextEncoder().encode(
`\x1b[1000D${line}\x1b[K`,
),
);
}
class Term {
constructor() {
}
blink(message: string): string {
return `\x1b[5m${message}\x1b[0m`;
}
bold(message: string): string {
return `\x1b[1m${message}\x1b[0m`;
}
red(message: string): string {
return `\x1b[31m${message}\x1b[0m`;
}
green(message: string): string {
return `\x1b[32m${message}\x1b[0m`;
}
yellow(message: string): string {
return `\x1b[33m${message}\x1b[0m`;
}
}
export const term = new Term();
function pad(message: string, length: number): string {
if (message.length >= length) {
return message;
}
return message + " ".repeat(length - message.length);
}
const LOGGER_PREFIX_LEN: number = 8;
class Logger {
constructor() {
}
// deno-lint-ignore no-explicit-any
success(...data: any[]) {
this.log(
term.bold(term.green(`[${pad("SUCCESS", LOGGER_PREFIX_LEN)}]`)),
data,
);
}
// deno-lint-ignore no-explicit-any
error(...data: any[]) {
this.log(
term.bold(term.red(`[${pad("ERROR", LOGGER_PREFIX_LEN)}]`)),
data,
);
}
// deno-lint-ignore no-explicit-any
warn(...data: any[]) {
this.log(
term.bold(term.yellow(`[${pad("WARN", LOGGER_PREFIX_LEN)}]`)),
data,
);
}
// deno-lint-ignore no-explicit-any
warning(...data: any[]) {
this.log(
term.blink(
term.bold(term.yellow(`[${pad("WARN", LOGGER_PREFIX_LEN)}]`)),
),
data,
);
}
// deno-lint-ignore no-explicit-any
info(...data: any[]) {
this.log(term.bold(`[${pad("INFO", LOGGER_PREFIX_LEN)}]`), data);
}
// deno-lint-ignore no-explicit-any
debug(...data: any[]) {
this.log(`[${pad("DEBUG", LOGGER_PREFIX_LEN)}]`, data);
}
// deno-lint-ignore no-explicit-any
log(prefix: string, data: any[]) {
const args = [prefix];
for (let i = 0; i < data.length; i++) {
args.push(data[i]);
}
console.log.apply(console, args);
}
}
export const log = new Logger();
export function getHomeDirOrDie(): string {
const homeDir = getHomeDir();
if (homeDir === null) {
throw new Error("Cannot find home dir");
}
return homeDir;
}
export function getHomeDir(): string | null {
if (Deno.build.os === "windows") {
const userProfile = Deno.env.get("USERPROFILE");
if (userProfile) {
return userProfile;
}
const homeDrive = Deno.env.get("HOMEDRIVE");
const homePath = Deno.env.get("HOMEPATH");
if (homeDrive && homePath) {
return homeDrive + homePath;
}
return null;
}
return Deno.env.get("HOME") || null;
}
export function resolveFilename(filename: string): string {
if (filename.startsWith("~/")) {
return getHomeDir() + filename.substring(1);
}
return filename;
}
export function joinPath(path1: string, ...paths: string[]): string {
let basePath = path1;
if (paths != null && paths.length > 0) {
for (let i = 0; i < paths.length; i++) {
const path2 = paths[i];
if (basePath.endsWith("/") && path2.startsWith("/")) {
basePath += path2.substring(1);
} else if (basePath.endsWith("/") || path2.startsWith("/")) {
basePath += path2;
} else {
basePath += "/" + path2;
}
}
}
return basePath;
}
export async function existsPath(path: string): Promise<boolean> {
try {
const stat = await Deno.stat(path);
return stat != null;
} catch {
return false;
}
}
export async function readFileToString(
filename: string,
): Promise<string | null> {
try {
return await Deno.readTextFile(resolveFilename(filename));
} catch (e) {
if (e instanceof Error && e.name == "NotFound") {
return null;
}
throw e;
}
}
export function readFileToStringSync(filename: string): string | null {
try {
return Deno.readTextFileSync(resolveFilename(filename));
} catch (e) {
if (e instanceof Error && e.name == "NotFound") {
return null;
}
throw e;
}
}
export async function writeStringToFile(
filename: string,
data: string | null,
): Promise<void> {
const newFilename = resolveFilename(filename);
if (data == null) {
if (await existsPath(newFilename)) {
await Deno.remove(newFilename);
}
} else {
const parentDirname = dirname(newFilename);
if (!await existsPath(parentDirname)) {
await Deno.mkdir(parentDirname, { recursive: true });
}
await Deno.writeTextFile(newFilename, data);
}
}
export function uint8ArrayToHexString(uint8: Uint8Array): string {
return Array.from(uint8)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
export function hexStringToUint8Array(hex: string): Uint8Array {
hex = hex.trim();
if (hex.startsWith("0x") || hex.startsWith("0X")) {
hex = hex.slice(2);
}
if (hex.length % 2 !== 0) {
throw new Error("Hex string must have an even number of characters");
}
if (!/^[0-9a-fA-F]*$/.test(hex)) {
throw new Error("Invalid hex string");
}
const byteLength = hex.length / 2;
const uint8 = new Uint8Array(byteLength);
for (let i = 0; i < byteLength; i++) {
uint8[i] = parseInt(hex.substring(i * 2, (i + 1) * 2), 16);
}
return uint8;
}
export function decodeBase64Url(base64UrlString: string): Uint8Array {
let standardBase64 = base64UrlString.replace(/-/g, "+").replace(/_/g, "/");
while (standardBase64.length % 4) {
standardBase64 += "=";
}
return decodeBase64(standardBase64);
}
export function encodeBase64Url(input: ArrayBufferLike): string {
let standardBased64 = encodeBase64(input);
return standardBased64.replace(/\+/g, "-").replace(/\//g, "_").replace(
/=/g,
"",
);
}
export function getKeyRingPassword(
service: string,
user: string,
): string | null {
const command = new Deno.Command("keyring.rs", {
args: ["-g", "--json", "-S", service, "-U", user],
});
const { code, stdout, stderr } = command.outputSync();
const stdoutString = new TextDecoder().decode(stdout);
const stderrString = new TextDecoder().decode(stderr);
if (code != 0) {
if (stderrString && stderrString.includes("Error: NoEntry")) {
return null;
}
throw new Error(
`keyring.rs -g failed, code: ${code}, stdout: ${stdoutString}, stderr: ${stderrString}`,
);
}
const result = JSON.parse(stdoutString) as {
password: string;
};
return result.password;
}
export function setKeyRingPassword(
service: string,
user: string,
password: string,
): void {
const command = new Deno.Command("keyring.rs", {
args: ["-s", "-S", service, "-U", user, "-P", password],
});
const { code, stdout, stderr } = command.outputSync();
const stdoutString = new TextDecoder().decode(stdout);
const stderrString = new TextDecoder().decode(stderr);
if (code != 0) {
throw new Error(
`keyring.rs -s failed, code: ${code}, stdout: ${stdoutString}, stderr: ${stderrString}`,
);
}
return;
}
Deno.test("isOn", () => {
assertEquals(false, isOn(undefined));
assertEquals(false, isOn(""));
assertEquals(true, isOn("true"));
assertEquals(true, isOn("TRUE"));
assertEquals(true, isOn("yes"));
assertEquals(true, isOn("YES"));
assertEquals(true, isOn("on"));
assertEquals(true, isOn("ON"));
assertEquals(true, isOn("1"));
});
Deno.test("formatHumanTime", () => {
assertEquals("0ms", formatHumanTime(0));
assertEquals("1ms", formatHumanTime(1));
assertEquals("1s", formatHumanTime(1000));
assertEquals("1s", formatHumanTime(1001));
assertEquals("1m", formatHumanTime(60001));
assertEquals("1m 1s", formatHumanTime(61001));
assertEquals("1h", formatHumanTime(3600000));
assertEquals("1h 1s", formatHumanTime(3601000));
assertEquals("1h 1m 1s", formatHumanTime(3661000));
});
Deno.test("formatSize", () => {
assertEquals("N/A", formatSize(-1));
assertEquals("0B", formatSize(0));
assertEquals("1B", formatSize(1));
assertEquals("1KiB", formatSize(1024));
assertEquals("1KiB 1B", formatSize(1024 + 1));
assertEquals("1MiB 1KiB 1B", formatSize(1024 * 1024 + 1024 + 1));
assertEquals(
"1GiB 1MiB 1KiB 1B",
formatSize(1024 * 1024 * 1024 + 1024 * 1024 + 1024 + 1),
);
});
Deno.test("formatSize2", () => {
assertEquals("N/A", formatSize2(-1));
assertEquals("0B", formatSize2(0));
assertEquals("1B", formatSize2(1));
assertEquals("1.00KiB", formatSize2(1024));
assertEquals("10.00KiB", formatSize2(1024 * 10));
assertEquals("1.00MiB", formatSize2(1024 * 1024));
});
Deno.test("formatPercent", () => {
assertEquals("N/A", formatPercent(100, -1));
assertEquals("N/A", formatPercent(100, 0));
assertEquals("N/A", formatPercent(100, 0));
assertEquals("10.00%", formatPercent(10, 100));
assertEquals("11.00%", formatPercent(11, 100));
assertEquals("1.10%", formatPercent(11, 1000));
assertEquals("0.10%", formatPercent(1, 1000));
assertEquals("0.00%", formatPercent(1, 100000));
assertEquals("100.00%", formatPercent(100, 100));
});
Deno.test("sleep", async () => {
const t1 = new Date().getTime();
await sleep(1000);
const t2 = new Date().getTime();
assert(Math.abs(1000 - (t2 - t1)) < 20);
});
Deno.test("base64Url", () => {
assertEquals(
"_dxhVwI3qd9fMBlpEMmi6Q",
encodeBase64Url(decodeBase64Url("_dxhVwI3qd9fMBlpEMmi6Q")),
);
assertEquals(
"1dxJeD7erjAYUNEmdVNE8KdhpPZs0pAHtb-kbSqYIe5j039PkTHbrQYOEoeEWN4UsDERhnUg7mY",
encodeBase64Url(
decodeBase64Url(
"1dxJeD7erjAYUNEmdVNE8KdhpPZs0pAHtb-kbSqYIe5j039PkTHbrQYOEoeEWN4UsDERhnUg7mY",
),
),
);
});
Deno.test("test-key-ring-rs", () => {
setKeyRingPassword("test-service", "test-user", "test-password");
assertEquals(
"test-password",
getKeyRingPassword("test-service", "test-user"),
);
});
Deno.test("join-path", () => {
assertEquals("a/b", joinPath("a/", "/b"));
assertEquals("a/b", joinPath("a/", "b"));
assertEquals("a/b", joinPath("a", "/b"));
assertEquals("a/b", joinPath("a", "b"));
assertEquals("a/b/c", joinPath("a", "b", "/c"));
assertEquals("a/b/c", joinPath("a", "b", "c"));
});