mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-12-11 17:41:26 -06:00
This eliminates a significant number of dependencies, eliminating all npm audit issues, speeding up `npm ci` by 20%, and overall making the build faster (faster startup, direct code is faster than streams, etc) and clearer to understand. I'm finding it much easier to make build changes for the module transform with this; I can more clearly indicate task dependencies and prevent running tasks that don't need to be run. Given we're changing our build process entirely (new deps, new steps), it seems like this is a good time to change things up.
255 lines
7.3 KiB
JavaScript
255 lines
7.3 KiB
JavaScript
/* eslint-disable no-restricted-globals */
|
|
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import ts from "../../lib/typescript.js";
|
|
import chalk from "chalk";
|
|
import which from "which";
|
|
import { spawn } from "child_process";
|
|
import assert from "assert";
|
|
|
|
/**
|
|
* 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]
|
|
*/
|
|
export async function exec(cmd, args, options = {}) {
|
|
return /**@type {Promise<{exitCode?: number}>}*/(new Promise((resolve, reject) => {
|
|
const { ignoreExitCode, waitForExit = true } = options;
|
|
|
|
if (!options.hidePrompt) console.log(`> ${chalk.green(cmd)} ${args.join(" ")}`);
|
|
const proc = spawn(which.sync(cmd), args, { stdio: waitForExit ? "inherit" : "ignore" });
|
|
if (waitForExit) {
|
|
proc.on("exit", exitCode => {
|
|
if (exitCode === 0 || ignoreExitCode) {
|
|
resolve({ exitCode: exitCode ?? undefined });
|
|
}
|
|
else {
|
|
reject(new Error(`Process exited with code: ${exitCode}`));
|
|
}
|
|
});
|
|
proc.on("error", error => {
|
|
reject(error);
|
|
});
|
|
}
|
|
else {
|
|
proc.unref();
|
|
// wait a short period in order to allow the process to start successfully before Node exits.
|
|
setTimeout(() => resolve({ exitCode: undefined }), 100);
|
|
}
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* @param {ts.Diagnostic[]} diagnostics
|
|
* @param {{ cwd?: string, pretty?: boolean }} [options]
|
|
*/
|
|
function formatDiagnostics(diagnostics, options) {
|
|
return options && options.pretty
|
|
? ts.formatDiagnosticsWithColorAndContext(diagnostics, getFormatDiagnosticsHost(options && options.cwd))
|
|
: ts.formatDiagnostics(diagnostics, getFormatDiagnosticsHost(options && options.cwd));
|
|
}
|
|
|
|
/**
|
|
* @param {ts.Diagnostic[]} diagnostics
|
|
* @param {{ cwd?: string }} [options]
|
|
*/
|
|
function reportDiagnostics(diagnostics, options) {
|
|
console.log(formatDiagnostics(diagnostics, { cwd: options && options.cwd, pretty: process.stdout.isTTY }));
|
|
}
|
|
|
|
/**
|
|
* @param {string | undefined} cwd
|
|
* @returns {ts.FormatDiagnosticsHost}
|
|
*/
|
|
function getFormatDiagnosticsHost(cwd) {
|
|
return {
|
|
getCanonicalFileName: fileName => fileName,
|
|
getCurrentDirectory: () => cwd ?? process.cwd(),
|
|
getNewLine: () => ts.sys.newLine,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reads JSON data with optional comments using the LKG TypeScript compiler
|
|
* @param {string} jsonPath
|
|
*/
|
|
export function readJson(jsonPath) {
|
|
const jsonText = fs.readFileSync(jsonPath, "utf8");
|
|
const result = ts.parseConfigFileTextToJson(jsonPath, jsonText);
|
|
if (result.error) {
|
|
reportDiagnostics([result.error]);
|
|
throw new Error("An error occurred during parse.");
|
|
}
|
|
return result.config;
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* Find the size of a directory recursively.
|
|
* Symbolic links can cause a loop.
|
|
* @param {string} root
|
|
* @returns {number} bytes
|
|
*/
|
|
export function getDirSize(root) {
|
|
const stats = fs.lstatSync(root);
|
|
|
|
if (!stats.isDirectory()) {
|
|
return stats.size;
|
|
}
|
|
|
|
return fs.readdirSync(root)
|
|
.map(file => getDirSize(path.join(root, file)))
|
|
.reduce((acc, num) => acc + num, 0);
|
|
}
|
|
|
|
class Deferred {
|
|
constructor() {
|
|
this.promise = new Promise((resolve, reject) => {
|
|
this.resolve = resolve;
|
|
this.reject = reject;
|
|
});
|
|
}
|
|
}
|
|
|
|
export class Debouncer {
|
|
/**
|
|
* @param {number} timeout
|
|
* @param {() => Promise<any>} action
|
|
*/
|
|
constructor(timeout, action) {
|
|
this._timeout = timeout;
|
|
this._action = action;
|
|
}
|
|
|
|
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;
|
|
};
|
|
}
|