1/* eslint-disable no-restricted-globals */ 2 3import fs from "fs"; 4import path from "path"; 5import chalk from "chalk"; 6import which from "which"; 7import { spawn } from "child_process"; 8import assert from "assert"; 9import JSONC from "jsonc-parser"; 10 11/** 12 * Executes the provided command once with the supplied arguments. 13 * @param {string} cmd 14 * @param {string[]} args 15 * @param {ExecOptions} [options] 16 * 17 * @typedef ExecOptions 18 * @property {boolean} [ignoreExitCode] 19 * @property {boolean} [hidePrompt] 20 * @property {boolean} [waitForExit=true] 21 */ 22export async function exec(cmd, args, options = {}) { 23 return /**@type {Promise<{exitCode?: number}>}*/(new Promise((resolve, reject) => { 24 const { ignoreExitCode, waitForExit = true } = options; 25 26 if (!options.hidePrompt) console.log(`> ${chalk.green(cmd)} ${args.join(" ")}`); 27 const proc = spawn(which.sync(cmd), args, { stdio: waitForExit ? "inherit" : "ignore" }); 28 if (waitForExit) { 29 proc.on("exit", exitCode => { 30 if (exitCode === 0 || ignoreExitCode) { 31 resolve({ exitCode: exitCode ?? undefined }); 32 } 33 else { 34 reject(new Error(`Process exited with code: ${exitCode}`)); 35 } 36 }); 37 proc.on("error", error => { 38 reject(error); 39 }); 40 } 41 else { 42 proc.unref(); 43 // wait a short period in order to allow the process to start successfully before Node exits. 44 setTimeout(() => resolve({ exitCode: undefined }), 100); 45 } 46 })); 47} 48 49/** 50 * Reads JSON data with optional comments using the LKG TypeScript compiler 51 * @param {string} jsonPath 52 */ 53export function readJson(jsonPath) { 54 const jsonText = fs.readFileSync(jsonPath, "utf8"); 55 return JSONC.parse(jsonText); 56} 57 58/** 59 * @param {string | string[]} source 60 * @param {string | string[]} dest 61 * @returns {boolean} 62 */ 63export function needsUpdate(source, dest) { 64 if (typeof source === "string" && typeof dest === "string") { 65 if (fs.existsSync(dest)) { 66 const {mtime: outTime} = fs.statSync(dest); 67 const {mtime: inTime} = fs.statSync(source); 68 if (+inTime <= +outTime) { 69 return false; 70 } 71 } 72 } 73 else if (typeof source === "string" && typeof dest !== "string") { 74 const {mtime: inTime} = fs.statSync(source); 75 for (const filepath of dest) { 76 if (fs.existsSync(filepath)) { 77 const {mtime: outTime} = fs.statSync(filepath); 78 if (+inTime > +outTime) { 79 return true; 80 } 81 } 82 else { 83 return true; 84 } 85 } 86 return false; 87 } 88 else if (typeof source !== "string" && typeof dest === "string") { 89 if (fs.existsSync(dest)) { 90 const {mtime: outTime} = fs.statSync(dest); 91 for (const filepath of source) { 92 if (fs.existsSync(filepath)) { 93 const {mtime: inTime} = fs.statSync(filepath); 94 if (+inTime > +outTime) { 95 return true; 96 } 97 } 98 else { 99 return true; 100 } 101 } 102 return false; 103 } 104 } 105 else if (typeof source !== "string" && typeof dest !== "string") { 106 for (let i = 0; i < source.length; i++) { 107 if (!dest[i]) { 108 continue; 109 } 110 if (fs.existsSync(dest[i])) { 111 const {mtime: outTime} = fs.statSync(dest[i]); 112 const {mtime: inTime} = fs.statSync(source[i]); 113 if (+inTime > +outTime) { 114 return true; 115 } 116 } 117 else { 118 return true; 119 } 120 } 121 return false; 122 } 123 return true; 124} 125 126export function getDiffTool() { 127 const program = process.env.DIFF; 128 if (!program) { 129 console.warn("Add the 'DIFF' environment variable to the path of the program you want to use."); 130 process.exit(1); 131 } 132 return program; 133} 134 135/** 136 * Find the size of a directory recursively. 137 * Symbolic links can cause a loop. 138 * @param {string} root 139 * @returns {number} bytes 140 */ 141export function getDirSize(root) { 142 const stats = fs.lstatSync(root); 143 144 if (!stats.isDirectory()) { 145 return stats.size; 146 } 147 148 return fs.readdirSync(root) 149 .map(file => getDirSize(path.join(root, file))) 150 .reduce((acc, num) => acc + num, 0); 151} 152 153class Deferred { 154 constructor() { 155 this.promise = new Promise((resolve, reject) => { 156 this.resolve = resolve; 157 this.reject = reject; 158 }); 159 } 160} 161 162export class Debouncer { 163 /** 164 * @param {number} timeout 165 * @param {() => Promise<any>} action 166 */ 167 constructor(timeout, action) { 168 this._timeout = timeout; 169 this._action = action; 170 } 171 172 enqueue() { 173 if (this._timer) { 174 clearTimeout(this._timer); 175 this._timer = undefined; 176 } 177 178 if (!this._deferred) { 179 this._deferred = new Deferred(); 180 } 181 182 this._timer = setTimeout(() => this.run(), 100); 183 return this._deferred.promise; 184 } 185 186 run() { 187 if (this._timer) { 188 clearTimeout(this._timer); 189 this._timer = undefined; 190 } 191 192 const deferred = this._deferred; 193 assert(deferred); 194 this._deferred = undefined; 195 try { 196 deferred.resolve(this._action()); 197 } 198 catch (e) { 199 deferred.reject(e); 200 } 201 } 202} 203 204const unset = Symbol(); 205/** 206 * @template T 207 * @param {() => T} fn 208 * @returns {() => T} 209 */ 210export function memoize(fn) { 211 /** @type {T | unset} */ 212 let value = unset; 213 return () => { 214 if (value === unset) { 215 value = fn(); 216 } 217 return value; 218 }; 219} 220