turbo-gulp
Version:
Gulp tasks to boost high-quality projects.
219 lines (185 loc) • 5.98 kB
text/typescript
import {
ChildProcess,
execFile as _execFile,
ExecFileOptions as _ExecFileOptions,
spawn as _spawn,
} from "child_process";
import * as fs from "fs";
import { Incident } from "incident";
import { PassThrough, Transform as TransformStream } from "stream";
import { promisify } from "util";
export interface ExecFileOptions {
cwd?: string;
env?: {[key: string]: string};
timeout?: number;
maxBuffer?: number;
killSignal?: string;
uid?: number;
gid?: number;
}
export interface ExecFileResult {
stdout: Buffer;
stderr: Buffer;
}
export interface ExecFileErrorData {
/**
* Executed command
*/
cmd: string;
killed: boolean;
/**
* Exit code: 0 if the execution was successful, else there was a runtime error
*/
code: number;
// TODO: check the type of `signal`
signal: null | any;
stdout: Buffer;
stderr: Buffer;
}
function asBuffer(val: string | Buffer): Buffer {
return val instanceof Buffer ? val : new Buffer(val, "utf8");
}
export class ExecFileError extends Incident<ExecFileErrorData, "ExecFileError", Error> {
constructor(nativeError: Error, stdout: Buffer | string, stderr: Buffer | string) {
const data: ExecFileErrorData = {
cmd: (<Error & {cmd: string}> nativeError).cmd,
killed: (<Error & {killed: boolean}> nativeError).killed,
code: (<Error & {code: number}> nativeError).code,
signal: (<Error & {signal: null | any}> nativeError).signal,
stdout: asBuffer(stdout),
stderr: asBuffer(stderr),
};
const message: string = `An error occured during the execution of: ${data.cmd}\n${nativeError.stack}`;
super(nativeError, "ExecFileError", data, message);
}
}
const _readFile: (filename: string, encoding: string) => Promise<string> = <any> promisify(fs.readFile);
const _writeFile: (filename: string, data: any) => Promise<any> = <any> promisify(fs.writeFile);
export async function readText(file: string): Promise<string> {
return _readFile(file, "utf8");
}
export async function writeText(file: string, text: string): Promise<void> {
return _writeFile(file, text);
}
export async function execFile(file: string, args: string[], options?: ExecFileOptions): Promise<ExecFileResult> {
return new Promise<ExecFileResult>((resolve, reject) => {
const normalizedOptions: _ExecFileOptions & {encoding: string} = {...options, encoding: "buffer"};
_execFile(
file,
args,
normalizedOptions,
(error: Error | null, stdout: Buffer | string, stderr: Buffer | string): void => {
if (error !== null) {
reject(new ExecFileError(error, stdout, stderr));
return;
}
const result: ExecFileResult = {
stdout: asBuffer(stdout),
stderr: asBuffer(stderr),
};
resolve(result);
},
);
});
}
export interface SpawnOptions {
cwd?: string;
env?: {[key: string]: string};
stdio?: "inherit" | "pipe";
/**
* Run in detached mode. Default: `false`.
*/
detached?: boolean;
}
export interface SpawnResult {
/**
* Buffer containing the whole standard output of the spawned process.
*/
stdout: Buffer;
/**
* Buffer containing the whole standard error of the spawned process.
*/
stderr: Buffer;
/**
* Exit value of the spawned process: a return code or exit signal.
*/
exit: Exit;
}
/**
* Exit value of a spawned process: a return code or exit signal
*/
export type Exit = SignalExit | CodeExit;
export interface CodeExit {
type: "code";
code: number;
}
export interface SignalExit {
type: "signal";
signal: string;
}
export class SpawnedProcess {
readonly process: ChildProcess;
private readonly stdoutChunks: Buffer[];
private readonly stderrChunks: Buffer[];
private exit?: Exit;
constructor(file: string, args: string[], options: SpawnOptions) {
this.stdoutChunks = [];
this.stderrChunks = [];
this.exit = undefined;
const detached: boolean = options.detached !== undefined ? options.detached : false;
this.process = _spawn(
file,
args,
{stdio: [process.stdin, "pipe", "pipe"], cwd: options.cwd, env: options.env, detached},
);
const stdout: TransformStream = new PassThrough();
this.process.stdout.pipe(stdout);
const stderr: TransformStream = new PassThrough();
this.process.stderr.pipe(stderr);
if (options.stdio === "inherit") {
stdout.pipe(process.stdout);
stderr.pipe(process.stderr);
}
stdout.on("data", (chunk: Buffer): void => {
this.stdoutChunks.push(chunk);
});
stderr.on("data", (chunk: Buffer): void => {
this.stderrChunks.push(chunk);
});
this.process.once("exit", (code: number | null, signal: string | null): void => {
if (code !== null) {
this.exit = {type: "code", code};
} else {
this.exit = {type: "signal", signal: signal!};
}
});
}
async toPromise(): Promise<SpawnResult> {
return new Promise<SpawnResult>((resolve: (res: SpawnResult) => void, reject) => {
if (this.exit !== undefined) {
const [stdout, stderr]: [Buffer, Buffer] = this.getBuffers();
resolve({stdout, stderr, exit: this.exit});
} else {
this.process.once("exit", (code: number | null, signal: string | null): void => {
let exit: Exit;
if (code !== null) {
exit = {type: "code", code};
} else {
exit = {type: "signal", signal: signal!};
}
const [stdout, stderr]: [Buffer, Buffer] = this.getBuffers();
resolve({stdout, stderr, exit});
});
}
});
}
private getBuffers(): [Buffer, Buffer] {
const stdout: Buffer = Buffer.concat(this.stdoutChunks);
const stderr: Buffer = Buffer.concat(this.stderrChunks);
this.stdoutChunks.length = 0;
this.stderrChunks.length = 0;
this.stdoutChunks.push(stdout);
this.stderrChunks.push(stderr);
return [stdout, stderr];
}
}