import { CancelError } from "@esfx/canceltoken"; import assert from "assert"; import { spawn } from "child_process"; import fs from "fs"; import JSONC from "jsonc-parser"; import pc from "picocolors"; 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(`> ${pc.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} */ this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } } export class Debouncer { /** * @param {number} timeout * @param {() => Promise | 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 }); }