TypeScript/scripts/build/utils.mjs

247 строки
7.0 KiB
JavaScript

import { CancelError } from "@esfx/canceltoken";
import assert from "assert";
import chalk from "chalk";
import { spawn } from "child_process";
import fs from "fs";
import JSONC from "jsonc-parser";
import which from "which";
/** @import { CancelToken } from "@esfx/canceltoken" */
void 0;
/**
* Executes the provided command once with the supplied arguments.
* @param {string} cmd
* @param {string[]} args
* @param {ExecOptions} [options]
*
* @typedef ExecOptions
* @property {boolean} [ignoreExitCode]
* @property {boolean} [hidePrompt]
* @property {boolean} [waitForExit=true]
* @property {boolean} [ignoreStdout]
* @property {CancelToken} [token]
*/
export async function exec(cmd, args, options = {}) {
return /**@type {Promise<{exitCode?: number}>}*/ (new Promise((resolve, reject) => {
const { ignoreExitCode, waitForExit = true, ignoreStdout } = options;
if (!options.hidePrompt) console.log(`> ${chalk.green(cmd)} ${args.join(" ")}`);
const proc = spawn(which.sync(cmd), args, { stdio: waitForExit ? ignoreStdout ? ["inherit", "ignore", "inherit"] : "inherit" : "ignore", detached: !waitForExit });
if (waitForExit) {
const onCanceled = () => {
proc.kill();
};
const subscription = options.token?.subscribe(onCanceled);
proc.on("exit", exitCode => {
if (exitCode === 0 || ignoreExitCode) {
resolve({ exitCode: exitCode ?? undefined });
}
else {
const reason = options.token?.signaled ? options.token.reason ?? new CancelError() :
new ExecError(exitCode);
reject(reason);
}
subscription?.unsubscribe();
});
proc.on("error", error => {
reject(error);
subscription?.unsubscribe();
});
}
else {
proc.unref();
resolve({ exitCode: undefined });
}
}));
}
export class ExecError extends Error {
exitCode;
/**
* @param {number | null} exitCode
* @param {string} message
*/
constructor(exitCode, message = `Process exited with code: ${exitCode}`) {
super(message);
this.exitCode = exitCode;
}
}
/**
* Reads JSON data with optional comments
* @param {string} jsonPath
*/
export function readJson(jsonPath) {
const jsonText = fs.readFileSync(jsonPath, "utf8");
/** @type {JSONC.ParseError[]} */
const errors = [];
const result = JSONC.parse(jsonText, errors);
if (errors.length) {
throw new Error(`Error parsing ${jsonPath}`);
}
return result;
}
/**
* @param {string | string[]} source
* @param {string | string[]} dest
* @returns {boolean}
*/
export function needsUpdate(source, dest) {
if (typeof source === "string" && typeof dest === "string") {
if (fs.existsSync(dest)) {
const { mtime: outTime } = fs.statSync(dest);
const { mtime: inTime } = fs.statSync(source);
if (+inTime <= +outTime) {
return false;
}
}
}
else if (typeof source === "string" && typeof dest !== "string") {
const { mtime: inTime } = fs.statSync(source);
for (const filepath of dest) {
if (fs.existsSync(filepath)) {
const { mtime: outTime } = fs.statSync(filepath);
if (+inTime > +outTime) {
return true;
}
}
else {
return true;
}
}
return false;
}
else if (typeof source !== "string" && typeof dest === "string") {
if (fs.existsSync(dest)) {
const { mtime: outTime } = fs.statSync(dest);
for (const filepath of source) {
if (fs.existsSync(filepath)) {
const { mtime: inTime } = fs.statSync(filepath);
if (+inTime > +outTime) {
return true;
}
}
else {
return true;
}
}
return false;
}
}
else if (typeof source !== "string" && typeof dest !== "string") {
for (let i = 0; i < source.length; i++) {
if (!dest[i]) {
continue;
}
if (fs.existsSync(dest[i])) {
const { mtime: outTime } = fs.statSync(dest[i]);
const { mtime: inTime } = fs.statSync(source[i]);
if (+inTime > +outTime) {
return true;
}
}
else {
return true;
}
}
return false;
}
return true;
}
export function getDiffTool() {
const program = process.env.DIFF;
if (!program) {
console.warn("Add the 'DIFF' environment variable to the path of the program you want to use.");
process.exit(1);
}
return program;
}
/**
* @template T
*/
export class Deferred {
constructor() {
/** @type {Promise<T>} */
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
}
export class Debouncer {
/**
* @param {number} timeout
* @param {() => Promise<any> | void} action
*/
constructor(timeout, action) {
this._timeout = timeout;
this._action = action;
}
get empty() {
return !this._deferred;
}
enqueue() {
if (this._timer) {
clearTimeout(this._timer);
this._timer = undefined;
}
if (!this._deferred) {
this._deferred = new Deferred();
}
this._timer = setTimeout(() => this.run(), 100);
return this._deferred.promise;
}
run() {
if (this._timer) {
clearTimeout(this._timer);
this._timer = undefined;
}
const deferred = this._deferred;
assert(deferred);
this._deferred = undefined;
try {
deferred.resolve(this._action());
}
catch (e) {
deferred.reject(e);
}
}
}
const unset = Symbol();
/**
* @template T
* @param {() => T} fn
* @returns {() => T}
*/
export function memoize(fn) {
/** @type {T | unset} */
let value = unset;
return () => {
if (value === unset) {
value = fn();
}
return value;
};
}
/**
* @param {fs.PathLike} p
*/
export function rimraf(p) {
// The rimraf package uses maxRetries=10 on Windows, but Node's fs.rm does not have that special case.
return fs.promises.rm(p, { recursive: true, force: true, maxRetries: process.platform === "win32" ? 10 : 0 });
}