1/* eslint-disable no-restricted-globals */ 2// eslint-disable-next-line @typescript-eslint/triple-slash-reference 3/// <reference path="../types/ambient.d.ts" /> 4 5import fs from "fs"; 6import path from "path"; 7import log from "fancy-log"; 8import del from "del"; 9import File from "vinyl"; 10import ts from "../../lib/typescript.js"; 11import chalk from "chalk"; 12import which from "which"; 13import { spawn } from "child_process"; 14import { Duplex } from "stream"; 15import assert from "assert"; 16 17/** 18 * Executes the provided command once with the supplied arguments. 19 * @param {string} cmd 20 * @param {string[]} args 21 * @param {ExecOptions} [options] 22 * 23 * @typedef ExecOptions 24 * @property {boolean} [ignoreExitCode] 25 * @property {boolean} [hidePrompt] 26 * @property {boolean} [waitForExit=true] 27 */ 28export async function exec(cmd, args, options = {}) { 29 return /**@type {Promise<{exitCode?: number}>}*/(new Promise((resolve, reject) => { 30 const { ignoreExitCode, waitForExit = true } = options; 31 32 if (!options.hidePrompt) log(`> ${chalk.green(cmd)} ${args.join(" ")}`); 33 const proc = spawn(which.sync(cmd), args, { stdio: waitForExit ? "inherit" : "ignore" }); 34 if (waitForExit) { 35 proc.on("exit", exitCode => { 36 if (exitCode === 0 || ignoreExitCode) { 37 resolve({ exitCode: exitCode ?? undefined }); 38 } 39 else { 40 reject(new Error(`Process exited with code: ${exitCode}`)); 41 } 42 }); 43 proc.on("error", error => { 44 reject(error); 45 }); 46 } 47 else { 48 proc.unref(); 49 // wait a short period in order to allow the process to start successfully before Node exits. 50 setTimeout(() => resolve({ exitCode: undefined }), 100); 51 } 52 })); 53} 54 55/** 56 * @param {ts.Diagnostic[]} diagnostics 57 * @param {{ cwd?: string, pretty?: boolean }} [options] 58 */ 59function formatDiagnostics(diagnostics, options) { 60 return options && options.pretty 61 ? ts.formatDiagnosticsWithColorAndContext(diagnostics, getFormatDiagnosticsHost(options && options.cwd)) 62 : ts.formatDiagnostics(diagnostics, getFormatDiagnosticsHost(options && options.cwd)); 63} 64 65/** 66 * @param {ts.Diagnostic[]} diagnostics 67 * @param {{ cwd?: string }} [options] 68 */ 69function reportDiagnostics(diagnostics, options) { 70 log(formatDiagnostics(diagnostics, { cwd: options && options.cwd, pretty: process.stdout.isTTY })); 71} 72 73/** 74 * @param {string | undefined} cwd 75 * @returns {ts.FormatDiagnosticsHost} 76 */ 77function getFormatDiagnosticsHost(cwd) { 78 return { 79 getCanonicalFileName: fileName => fileName, 80 getCurrentDirectory: () => cwd ?? process.cwd(), 81 getNewLine: () => ts.sys.newLine, 82 }; 83} 84 85/** 86 * Reads JSON data with optional comments using the LKG TypeScript compiler 87 * @param {string} jsonPath 88 */ 89export function readJson(jsonPath) { 90 const jsonText = fs.readFileSync(jsonPath, "utf8"); 91 const result = ts.parseConfigFileTextToJson(jsonPath, jsonText); 92 if (result.error) { 93 reportDiagnostics([result.error]); 94 throw new Error("An error occurred during parse."); 95 } 96 return result.config; 97} 98 99/** 100 * @param {string | string[]} source 101 * @param {string | string[]} dest 102 * @returns {boolean} 103 */ 104export function needsUpdate(source, dest) { 105 if (typeof source === "string" && typeof dest === "string") { 106 if (fs.existsSync(dest)) { 107 const {mtime: outTime} = fs.statSync(dest); 108 const {mtime: inTime} = fs.statSync(source); 109 if (+inTime <= +outTime) { 110 return false; 111 } 112 } 113 } 114 else if (typeof source === "string" && typeof dest !== "string") { 115 const {mtime: inTime} = fs.statSync(source); 116 for (const filepath of dest) { 117 if (fs.existsSync(filepath)) { 118 const {mtime: outTime} = fs.statSync(filepath); 119 if (+inTime > +outTime) { 120 return true; 121 } 122 } 123 else { 124 return true; 125 } 126 } 127 return false; 128 } 129 else if (typeof source !== "string" && typeof dest === "string") { 130 if (fs.existsSync(dest)) { 131 const {mtime: outTime} = fs.statSync(dest); 132 for (const filepath of source) { 133 if (fs.existsSync(filepath)) { 134 const {mtime: inTime} = fs.statSync(filepath); 135 if (+inTime > +outTime) { 136 return true; 137 } 138 } 139 else { 140 return true; 141 } 142 } 143 return false; 144 } 145 } 146 else if (typeof source !== "string" && typeof dest !== "string") { 147 for (let i = 0; i < source.length; i++) { 148 if (!dest[i]) { 149 continue; 150 } 151 if (fs.existsSync(dest[i])) { 152 const {mtime: outTime} = fs.statSync(dest[i]); 153 const {mtime: inTime} = fs.statSync(source[i]); 154 if (+inTime > +outTime) { 155 return true; 156 } 157 } 158 else { 159 return true; 160 } 161 } 162 return false; 163 } 164 return true; 165} 166 167export function getDiffTool() { 168 const program = process.env.DIFF; 169 if (!program) { 170 log.warn("Add the 'DIFF' environment variable to the path of the program you want to use."); 171 process.exit(1); 172 } 173 return program; 174} 175 176/** 177 * Find the size of a directory recursively. 178 * Symbolic links can cause a loop. 179 * @param {string} root 180 * @returns {number} bytes 181 */ 182export function getDirSize(root) { 183 const stats = fs.lstatSync(root); 184 185 if (!stats.isDirectory()) { 186 return stats.size; 187 } 188 189 return fs.readdirSync(root) 190 .map(file => getDirSize(path.join(root, file))) 191 .reduce((acc, num) => acc + num, 0); 192} 193 194/** 195 * @param {string | ((file: File) => string) | { cwd?: string }} [dest] 196 * @param {{ cwd?: string }} [opts] 197 */ 198export function rm(dest, opts) { 199 if (dest && typeof dest === "object") { 200 opts = dest; 201 dest = undefined; 202 } 203 let failed = false; 204 205 const cwd = path.resolve(opts && opts.cwd || process.cwd()); 206 207 /** @type {{ file: File, deleted: boolean, promise: Promise<any>, cb: Function }[]} */ 208 const pending = []; 209 210 const processDeleted = () => { 211 if (failed) return; 212 while (pending.length && pending[0].deleted) { 213 const fileAndCallback = pending.shift(); 214 assert(fileAndCallback); 215 const { file, cb } = fileAndCallback; 216 duplex.push(file); 217 cb(); 218 } 219 }; 220 221 const duplex = new Duplex({ 222 objectMode: true, 223 /** 224 * @param {string|Buffer|File} file 225 */ 226 write(file, _, cb) { 227 if (failed) return; 228 if (typeof file === "string" || Buffer.isBuffer(file)) return cb(new Error("Only Vinyl files are supported.")); 229 const basePath = typeof dest === "string" ? path.resolve(cwd, dest) : 230 typeof dest === "function" ? path.resolve(cwd, dest(file)) : 231 file.base; 232 const filePath = path.resolve(basePath, file.relative); 233 file.cwd = cwd; 234 file.base = basePath; 235 file.path = filePath; 236 const entry = { 237 file, 238 deleted: false, 239 cb, 240 promise: del(file.path).then(() => { 241 entry.deleted = true; 242 processDeleted(); 243 }, err => { 244 failed = true; 245 pending.length = 0; 246 cb(err); 247 }) 248 }; 249 pending.push(entry); 250 }, 251 final(cb) { 252 // eslint-disable-next-line no-null/no-null 253 const endThenCb = () => (duplex.push(null), cb()); // signal end of read queue 254 processDeleted(); 255 if (pending.length) { 256 Promise 257 .all(pending.map(entry => entry.promise)) 258 .then(() => processDeleted()) 259 .then(() => endThenCb(), endThenCb); 260 return; 261 } 262 endThenCb(); 263 }, 264 read() { 265 } 266 }); 267 return duplex; 268} 269 270class Deferred { 271 constructor() { 272 this.promise = new Promise((resolve, reject) => { 273 this.resolve = resolve; 274 this.reject = reject; 275 }); 276 } 277} 278 279export class Debouncer { 280 /** 281 * @param {number} timeout 282 * @param {() => Promise<any>} action 283 */ 284 constructor(timeout, action) { 285 this._timeout = timeout; 286 this._action = action; 287 } 288 289 enqueue() { 290 if (this._timer) { 291 clearTimeout(this._timer); 292 this._timer = undefined; 293 } 294 295 if (!this._deferred) { 296 this._deferred = new Deferred(); 297 } 298 299 this._timer = setTimeout(() => this.run(), 100); 300 return this._deferred.promise; 301 } 302 303 run() { 304 if (this._timer) { 305 clearTimeout(this._timer); 306 this._timer = undefined; 307 } 308 309 const deferred = this._deferred; 310 assert(deferred); 311 this._deferred = undefined; 312 try { 313 deferred.resolve(this._action()); 314 } 315 catch (e) { 316 deferred.reject(e); 317 } 318 } 319} 320