• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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