Files
ts-scripts/libraries/deno-commons-mod.ts
2026-02-12 01:34:38 +08:00

1115 lines
31 KiB
TypeScript

// Reference:
// - https://docs.deno.com/runtime/fundamentals/testing/
import {decodeBase64, encodeBase64} from "jsr:@std/encoding/base64";
import {dirname, fromFileUrl} from "jsr:@std/path";
import {toArrayBuffer} from "jsr:@std/streams";
import {spawn, SpawnOptionsWithoutStdio} from "node:child_process";
import {mkdir, readFile, readFileSync, rm, writeFile} from "node:fs";
// 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 function isDeno(): boolean {
return typeof Deno !== "undefined";
}
export function args(): string[] {
return isDeno() ? Deno.args : process.argv.slice(2);
}
export function osEnv(key: string): string | undefined {
return isDeno() ? Deno.env.get(key) : process.env[key];
}
export function stdin(): ReadableStream {
return isDeno() ? Deno.stdin.readable : process.stdin;
}
function streamToArrayBuffer(stream) {
const chunks = [];
return new Promise((resolve, reject) => {
stream.on("error", (err) => reject(err));
stream.on("data", (chunk) => {
chunks.push(chunk);
});
stream.on("end", () => {
const buffer = Buffer.concat(chunks);
resolve(
buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength,
),
);
});
});
}
export function stdinToArrayBuffer(): Promise<ArrayBuffer> {
if (isDeno()) {
return toArrayBuffer(Deno.stdin.readable);
}
return streamToArrayBuffer(process.stdin);
}
export function exit(code?: number): never {
isDeno() ? Deno.exit(code) : process.exit(code);
}
export class ProcessOutput {
code: number;
stdout: string;
stderr: string;
constructor(code: number, stdout: string, stderr: string) {
this.code = code;
this.stdout = stdout;
this.stderr = stderr;
}
assertSuccess(): ProcessOutput {
if (this.code !== 0) {
throw new Error(
`Failed to execute command, exit code: ${this.code}\n- stdout: ${this.stdout}\n- stderr: ${this.stderr}\n`,
);
}
return this;
}
stdoutThenTrim(): string {
return this.getStdoutAsStringThenTrim();
}
getStdoutAsStringThenTrim(): string {
return this.stdout.trim();
}
stderrThenTrim(): string {
return this.getStderrAsStringThenTrim();
}
getStderrAsStringThenTrim(): string {
return this.stderr.trim();
}
stdoutAsJson(): any {
return this.getStdoutAsJson();
}
getStdoutAsJson(): any {
return JSON.parse(this.stdout);
}
stderrAsJson(): any {
return this.getStderrAsJson();
}
getStderrAsJson(): any {
return JSON.parse(this.stderr);
}
}
export async function execCommandAndStdout(
command: string,
args?: string[],
options?: Deno.CommandOptions | SpawnOptionsWithoutStdio,
): Promise<string> {
const processOutput = await execCommand(command, args, options);
processOutput.assertSuccess();
return processOutput.stdout.trim();
}
export async function execCommand(
command: string,
args?: string[],
options?: Deno.CommandOptions | SpawnOptionsWithoutStdio,
): Promise<ProcessOutput> {
if (isDeno()) {
const opts = options || {};
if (args) opts.args = args;
const cmd = new Deno.Command(command, opts);
const { code, stdout, stderr } = await cmd.output();
return new ProcessOutput(
code,
new TextDecoder().decode(stdout),
new TextDecoder().decode(stderr),
);
}
return await execCommandSpawn(command, args, options);
}
async function execCommandSpawn(
command: string,
args: string[],
options?: SpawnOptionsWithoutStdio,
): Promise<ProcessOutput> {
const ps = spawn(command, args, options);
return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
ps.stdout.on("data", (data) => {
stdout += data.toString();
});
ps.stderr.on("data", (data) => {
stderr += data.toString();
});
ps.on("close", (code) => {
try {
const output = new ProcessOutput(code, stdout, stderr);
ps.stdin.end();
resolve(output);
} catch (e) {
reject(e);
}
});
ps.on("error", (err) => {
reject(err);
});
});
}
export async function execCommandShell(
command: string,
args?: string[],
options?: Deno.CommandOptions | SpawnOptionsWithoutStdio,
): Promise<number> {
if (isDeno()) {
const opts = options || {};
if (args) opts.args = args;
opts.stdin = "inherit";
opts.stdout = "inherit";
opts.stderr = "inherit";
const cmd = new Deno.Command(command, opts);
return (await cmd.spawn().status).code;
}
return await execCommandShellSpwan(command, args, options);
}
async function execCommandShellSpwan(
command: string,
args?: string[],
options?: SpawnOptionsWithoutStdio,
): Promise<number> {
return new Promise((resolve, reject) => {
const ps = spawn(command, args, {
shell: false,
stdio: ["inherit", "inherit", "inherit"],
});
ps.on("close", (code) => {
resolve(code);
});
ps.on("error", (err) => {
reject(err);
});
});
}
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 isUndefined(val: any): boolean {
return typeof val === "undefined";
}
export function isUndefinedOrNull(val: any): boolean {
return isUndefined(val) || (val === null);
}
export function parseIntVal(val: any, defaultVal: number): number {
if (isUndefinedOrNull(val)) {
return defaultVal;
}
const parsedVal = parseInt(val, 10);
if (isNaN(parsedVal)) {
return defaultVal;
}
return parsedVal;
}
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 osEnv(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) {
const encodedLineBytes = new TextEncoder().encode(
`\x1b[1000D${line}\x1b[K`,
);
if (isDeno()) {
await Deno.stdout.write(encodedLineBytes);
} else {
process.stdout.write(encodedLineBytes);
}
}
interface ColorToken {
type: "color" | "text";
content?: string;
colorStart?: boolean;
color?: string;
}
function parseColorTokens(message: string, renderColor: boolean): ColorToken[] {
const tokens: ColorToken[] = [];
if (message) {
let inColorStart = false;
let inColorEnd = false;
let noEscape = false;
let chars: string[] = [];
let startedColors: string[] = [];
const messageLength = message.length;
const _isAllSlashOrEmpty = function (chars: string): {
result: boolean;
count: number;
} {
for (const char of chars) {
if (char !== "/") {
return { result: false, count: -1 };
}
}
return { result: true, count: chars.length };
};
for (let i = 0; i < messageLength; i++) {
const c = message.charAt(i);
const nextC = (i + 1) < messageLength
? message.charAt(i + 1)
: null;
const nextNextC = (i + 2) < messageLength
? message.charAt(i + 2)
: null;
if (noEscape) {
if (c === "]" && nextC === "]" && nextNextC === "]") {
// end no escape
noEscape = false;
i += 2;
} else {
chars.push(c);
}
continue;
}
switch (c) {
case "\\":
if (nextC === null) {
chars.push(c);
} else {
chars.push(nextC);
i++;
}
break;
case "[":
if (inColorStart || inColorEnd) {
// SHOULD NOT HAPPEN
break;
} else if (nextC == "/") {
inColorEnd = true;
i++;
} else if (nextC == "[" && nextNextC == "[") {
// now no escape
noEscape = true;
i += 2;
break;
} else {
inColorStart = true;
}
if (chars.length > 0) {
tokens.push({
type: "text",
content: chars.join(""),
});
chars = [];
}
break;
case "]":
if (inColorStart || inColorEnd) {
const isAllSlashOrEmpty = _isAllSlashOrEmpty(chars);
if (isAllSlashOrEmpty.result && inColorEnd) {
const popCount = isAllSlashOrEmpty.count + 1;
for (let _i = 0; _i < popCount; _i++) {
const poppedColor = startedColors.pop();
if (poppedColor) {
tokens.push({
type: "color",
colorStart: false,
color: poppedColor,
});
}
}
chars = [];
} else if (chars.length > 0) {
tokens.push({
type: "color",
colorStart: inColorStart,
color: chars.join(""),
});
if (inColorStart) {
startedColors.push(chars.join(""));
} else {
startedColors.pop();
}
chars = [];
}
inColorStart = false;
inColorEnd = false;
} else {
chars.push(c);
}
break;
default:
chars.push(c);
break;
}
}
const inColor = inColorStart || inColorEnd;
if (chars.length > 0 && !inColor) {
tokens.push({
type: "text",
content: chars.join(""),
});
}
}
return tokens;
}
const COLOR_MAP: Record<string, string> = {
blink: "5",
bold: "1",
b: "1",
under: "4",
u: "4",
strikeout: "9",
s: "9",
black: "30",
red: "31",
green: "32",
yellow: "33",
blue: "34",
pink: "35",
cyan: "36",
white: "37",
bg_black: "40",
bg_red: "41",
bg_green: "42",
bg_yellow: "43",
bg_blue: "44",
bg_pink: "45",
bg_cyan: "46",
bg_white: "47",
black_bright: "90",
red_bright: "91",
green_bright: "92",
yellow_bright: "93",
blue_bright: "94",
pink_bright: "95",
cyan_bright: "96",
white_bright: "97",
bg_black_bright: "100",
bg_red_bright: "101",
bg_green_bright: "102",
bg_yellow_bright: "103",
bg_blue_bright: "104",
bg_pink_bright: "105",
bg_cyan_bright: "106",
bg_white_bright: "107",
};
function getColorCode(color: string): string {
if (color.startsWith("#")) {
return color.substring(1);
}
return COLOR_MAP[color];
}
function renderColorTokens(tokens: ColorToken[]): string {
const text: string[] = [];
const colorMapStack = new Map<string, number[]>();
for (const token of tokens) {
if (token.type === "color") {
const color = token.color;
if (color) {
const colorCode = getColorCode(color);
if (!colorCode) {
text.push(`[${token.colorStart ? "" : "/"}${color}]`);
continue;
}
const colorStack = colorMapStack.get(color) ?? [];
if (colorStack.length == 0) {
colorMapStack.set(color, colorStack);
}
if (token.colorStart) {
colorStack.push(1);
} else {
colorStack.pop();
text.push("\x1b[0m");
}
const colors: string[] = [];
for (const [color, colorStack] of colorMapStack) {
if (colorStack.length > 0) {
const currentColorCode = getColorCode(color);
if (currentColorCode) {
colors.push(currentColorCode);
}
}
}
if (colors.length > 0) {
text.push(`\x1b[${colors.join(";")}m`);
}
}
} else {
if (token.content) {
text.push(token.content);
}
}
}
text.push("\x1b[0m"); // FINALLY END ALL COLOR
return text.join("");
}
export function supportColor(): boolean {
try {
if (process.env.FORCE_COLOR !== undefined) {
return process.env.FORCE_COLOR !== "0";
}
if (process.env.NO_COLOR !== undefined) {
return false;
}
return process.stdout.isTTY && process.stderr.isTTY;
} catch (e) {
// check color support failed, default false
return false;
}
}
class Term {
constructor() {
}
blink(message: string): string {
return `\x1b[5m${message}\x1b[0m`;
}
bold(message: string): string {
return `\x1b[1m${message}\x1b[0m`;
}
under(message: string): string {
return `\x1b[4m${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`;
}
blue(message: string): string {
return `\x1b[34m${message}\x1b[0m`;
}
pink(message: string): string {
return `\x1b[35m${message}\x1b[0m`;
}
cyan(message: string): string {
return `\x1b[36m${message}\x1b[0m`;
}
auto(message: string, renderColor?: boolean): string {
return renderColorTokens(
parseColorTokens(message),
renderColor ?? supportColor(),
);
}
}
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 {
_debug: boolean = false;
constructor() {
this._debug = osEnv("LOGGER") === "*";
}
// 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[]) {
if (this._debug) {
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 {
return homeDirOrDie();
}
export function homeDirOrDie(): string {
const homeDir = getHomeDir();
if (homeDir === null) {
throw new Error("Cannot find home dir");
}
return homeDir;
}
export function homeDir(): string | null {
return getHomeDir();
}
export function getHomeDir(): string | null {
// if (Deno.build.os === "windows") {
// const userProfile = osEnv("USERPROFILE");
// if (userProfile) {
// return userProfile;
// }
// const homeDrive = osEnv("HOMEDRIVE");
// const homePath = osEnv("HOMEPATH");
// if (homeDrive && homePath) {
// return homeDrive + homePath;
// }
// return null;
// }
return osEnv("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> {
if (isDeno()) {
try {
return await Deno.stat(path) != null;
} catch {
return false;
}
}
return new Promise((resolve) => {
fs.stat(path, (err, stats) => {
err ? resolve(false) : resolve(stats !== null);
});
});
}
function isDenoNotFound(e) {
return e instanceof Error && e.name == "NotFound";
}
function isNodeNotFound(e) {
return (e.errno ?? 0 === -2) && e.message &&
e.message.includes("no such file or directory");
}
export async function readFileToString(
filename: string,
): Promise<string | null> {
if (isDeno()) {
try {
return await Deno.readTextFile(resolveFilename(filename));
} catch (e) {
if (isDenoNotFound(e)) {
return null;
}
throw e;
}
}
return new Promise((resolve, reject) => {
readFile(resolveFilename(filename), "utf8", (err, buffer) => {
if (err && isNodeNotFound(err)) {
resolve(null);
} else {
err ? reject(err) : resolve(buffer);
}
});
});
}
export function readFileToStringSync(filename: string): string | null {
if (isDeno()) {
try {
return Deno.readTextFileSync(resolveFilename(filename));
} catch (e) {
if (isDenoNotFound(e)) {
return null;
}
throw e;
}
}
try {
return readFileSync(resolveFilename(filename), "utf8");
} catch (err) {
if (isNodeNotFound(err)) {
return null;
}
throw err;
}
}
export async function writeStringToFile(
filename: string,
data: string | null,
): Promise<void> {
const newFilename = resolveFilename(filename);
if (data == null) {
if (await existsPath(newFilename)) {
await removePath(newFilename);
}
} else {
const parentDirname = dirname(newFilename);
if (!await existsPath(parentDirname)) {
await makeDirectory(parentDirname);
}
if (isDeno()) {
await Deno.writeTextFile(newFilename, data);
} else {
return new Promise((resolve, reject) => {
writeFile(newFilename, data, (err) => {
err ? reject(err) : resolve();
});
});
}
}
}
export async function removePath(path: string): Promise<void> {
if (isDeno()) {
await Deno.remove(newFilename);
} else {
return new Promise((resolve, reject) => {
rm(path, (err) => {
err ? reject(err) : resolve();
});
});
}
}
export async function makeDirectory(
directory: string,
recursive?: boolean,
): Promise<void> {
if (isDeno()) {
await Deno.mkdir(parentDirname, { recursive: recursive ?? true });
} else {
return new Promise((resolve, reject) => {
mkdir(directory, { recursive: recursive ?? true }, (err) => {
err ? reject(err) : resolve();
});
});
}
}
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: ArrayBuffer | Uint8Array | string,
): string {
let standardBased64 = encodeBase64(input);
return standardBased64.replace(/\+/g, "-").replace(/\//g, "_").replace(
/=/g,
"",
);
}
export async function getKeyRingPassword(
service: string,
user: string,
): Promise<string | null> {
const keyRingArgs = ["-g", "--json", "-U", user];
if (service) {
keyRingArgs.push(...["-S", service]);
}
const processOutput = await execCommand("keyring.rs", keyRingArgs);
const stdoutString = processOutput.getStdoutAsStringThenTrim();
const stderrString = processOutput.getStderrAsStringThenTrim();
if (processOutput.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 async function setKeyRingPassword(
service: string,
user: string,
password: string,
): Promise<void> {
const keyRingArgs = ["-s", "-U", user, "-P", password];
if (service) {
keyRingArgs.push(...["-S", service]);
}
const processOutput = await execCommand("keyring.rs", keyRingArgs);
const stdoutString = processOutput.getStdoutAsStringThenTrim();
const stderrString = processOutput.getStderrAsStringThenTrim();
if (processOutput.code != 0) {
throw new Error(
`keyring.rs -s failed, code: ${code}, stdout: ${stdoutString}, stderr: ${stderrString}`,
);
}
return;
}
export class ProcessBar {
interval?: number;
message: string;
constructor(message?: string) {
this.message = message || "Processing";
}
async call<T>(cb: () => Promise<T>, clearLine?: boolean): Promise<T> {
this.start();
try {
return await cb();
} finally {
this.stop(clearLine);
}
}
start(): void {
const startMs = new Date().getTime();
let count = 0;
this.interval = setInterval(() => {
const dots = ".".repeat(((count++) % 10) + 1);
const costMs = new Date().getTime() - startMs;
const time = `${Math.floor(costMs / 1000)}s`;
process.stderr.write(`\r${this.message} ${time} ${dots}\x1b[K`);
}, 500);
}
stop(clearLine?: boolean): void {
if (this.interval) {
clearInterval(this.interval);
process.stderr.write(clearLine ? "\r\x1b[K" : "\n");
}
}
}
/**
* @deprecated Use {@link fetchDataWithTimeout} instead.
*/
export async function fetchWithTimeout(
input: URL | Request | string,
timeout?: number,
initCallback?: (init: RequestInit) => RequestInit,
): Promise<Response> {
const fetchTimeout = timeout || 10000;
const abortController = new AbortController();
const timeoutHandler = setTimeout(() => {
abortController.abort(`Timeout ${fetchTimeout} ms`);
}, fetchTimeout);
let init: RequestInit = {};
init.signal = abortController.signal;
if (initCallback) {
init = initCallback(init);
}
const response = await fetch(input, init);
clearTimeout(timeoutHandler);
return response;
}
export type RequestInitWithTimeout = RequestInit & {
timeoutMillis?: number;
};
export async function fetchDataWithTimeout(
input: URL | Request | string,
init: RequestInitWithTimeout = {},
): Promise<Response> {
const timeout = init.timeoutMillis ?? 10000;
const abortController = new AbortController();
const timeoutHandler = setTimeout(() => {
abortController.abort(
`Fetch '${input}' timeout: ${timeout} ms`,
);
}, timeout);
try {
return await fetch(input, {
...init,
signal: abortController.signal,
});
} finally {
clearTimeout(timeoutHandler);
}
}
export function getCurrentScriptFile(): string {
return fromFileUrl(import.meta.url);
}
export function getCurrentScriptDirectory(): string {
return dirname(getCurrentScriptFile());
}
export function stringifySorted<T extends Record<string, any>>(
record: T,
space?: string | number,
): string {
return JSON.stringify(record, (key, value) => {
if (
value !== null && typeof value === "object" && !Array.isArray(value)
) {
const sortedKeys = Object.keys(value).sort();
const sortedObj: Record<string, any> = {};
for (const k of sortedKeys) {
sortedObj[k] = value[k];
}
return sortedObj;
}
return value;
}, space);
}
export function stringifyPretty(object: any): string {
return JSON.stringify(object, null, 2);
}