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