import chalk from "chalk"; import spawn from "cross-spawn"; import { Cancelable, debounce } from "lodash"; import { Executor } from "./types"; interface CommandExecutorOptions { args?: ReadonlyArray; /** If true, allow spawning multiple processes. */ spawnMutiple?: boolean; /** Specify the period in which the process is started at max once. */ debounce?: number | false; /** If true, will run command upon initialization. */ runOnInit?: boolean; } export default class CommandExecutor implements Executor { private cmd: string; private args?: ReadonlyArray; private spawnMultiple: boolean; private runOnInit: boolean; private isRunning = false; private shouldRespawn = false; private spawnProcessDebounced?: (() => void) & Cancelable; constructor(cmd: string, opts: CommandExecutorOptions = {}) { this.cmd = cmd; this.args = opts.args; this.spawnMultiple = opts.spawnMutiple || false; this.runOnInit = opts.runOnInit || false; const wait = opts.debounce === undefined ? 500 : opts.debounce; if (wait) { this.spawnProcessDebounced = debounce(() => this.spawnProcess(), wait); } } // This is called before watching starts. public onInit(): void { if (this.runOnInit) { this.spawnProcessPotentiallyDebounced(); } } private spawnProcessPotentiallyDebounced() { if (this.spawnProcessDebounced) { this.spawnProcessDebounced(); return; } this.spawnProcess(); } private spawnProcess() { if (this.isRunning && !this.spawnMultiple) { this.shouldRespawn = true; return; } this.isRunning = true; this.shouldRespawn = false; const child = spawn(this.cmd, this.args as string[], { stdio: "inherit", shell: !this.args, }); child.on("close", (code: number) => { this.isRunning = false; if (code !== 0 && code !== null) { // eslint-disable-next-line no-console console.log(chalk.red(`Command exited with ${code}`)); } if (this.shouldRespawn) { this.spawnProcessPotentiallyDebounced(); } }); } public execute(filePath: string) { this.spawnProcessPotentiallyDebounced(); } }