247 строки
7.0 KiB
JavaScript
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 });
|
|
}
|