import * as child_process from "node:child_process"; import * as fs from "node:fs/promises"; import * as path from "node:path"; import { bsc_exe, rescript_exe } from "#cli/bins"; /** * @typedef {{ * throwOnFail?: boolean, * } & child_process.SpawnOptions} ExecOptions * * @typedef {{ * status: number, * stdout: string, * stderr: string, * }} ExecResult */ const signals = { SIGINT: 2, SIGQUIT: 3, SIGKILL: 9, SIGTERM: 15, }; export const { shell, node, npm, yarn, mocha, bsc, execBin, rescript, execBuild, execBuildOrThrow, execClean, } = setup(); /** * @param {string} [cwd] */ export function setup(cwd = process.cwd()) { /** * @param {string} command * @param {string[]} [args] * @param {ExecOptions} [options] * @return {Promise} */ async function exec(command, args = [], options = {}) { const { throwOnFail = options.stdio === "inherit" } = options; const stdoutChunks = []; const stderrChunks = []; const subprocess = child_process.spawn(command, args, { cwd, shell: process.platform === "win32", stdio: ["ignore", "pipe", "pipe"], ...options, }); subprocess.stdout?.on("data", chunk => { stdoutChunks.push(chunk); }); subprocess.stderr?.on("data", chunk => { stderrChunks.push(chunk); }); return await new Promise((resolve, reject) => { subprocess.once("error", err => { reject(err); }); subprocess.once("close", (exitCode, signal) => { const stdout = Buffer.concat(stdoutChunks).toString("utf8"); const stderr = Buffer.concat(stderrChunks).toString("utf8"); let code = exitCode ?? 1; if (signals[signal]) { // + 128 is standard POSIX practice, see also https://nodejs.org/api/process.html#exit-codes code = signals[signal] + 128; } if (throwOnFail && code !== 0) { reject( new Error( `Command ${command} exited with non-zero status: ${code}`, ), ); } else { resolve({ status: code, stdout, stderr }); } }); }); } return { /** * bash shell script * * @param {string} script * @param {string[]} [args] * @param {ExecOptions} [options] * @return {Promise} */ shell(script, args = [], options = {}) { return exec("bash", [script, ...args], options); }, /** * Execute JavaScript on Node.js * * @param {string} script * @param {string[]} [args] * @param {ExecOptions} [options] * @return {Promise} */ node(script, args = [], options = {}) { return exec("node", [script, ...args], options); }, /** * Execute npm command * * @param {string} command * @param {string[]} [args] * @param {ExecOptions} [options] * @return {Promise} */ npm(command, args = [], options = {}) { return exec("npm", [...command.split(" "), ...args], options); }, /** * Execute Yarn command * * @param {string} command * @param {string[]} [args] * @param {ExecOptions} [options] * @return {Promise} */ yarn(command, args = [], options = {}) { return exec("yarn", [...command.split(" "), ...args], options); }, /** * Execute Mocha CLI * * @param {string[]} [args] * @param {ExecOptions} [options] * @return {Promise} */ mocha(args = [], options = {}) { // `yarn mocha` works, but format output differently // No more efforts here since we're plannig to drop Mocha return exec("npx", ["mocha", ...args], options); }, /** * `bsc` CLI * * @param {string[]} [args] * @param {ExecOptions} [options] * @return {Promise} */ bsc(args = [], options = {}) { return exec(bsc_exe, args, options); }, /** * `rescript` CLI * * @param {( * | "build" * | "clean" * | "format" * | (string & {}) * )} command * @param {string[]} [args] * @param {ExecOptions} [options] * @return {Promise} */ rescript(command, args = [], options = {}) { const cliPath = path.join(import.meta.dirname, "../cli/rescript.js"); return exec("node", [cliPath, command, ...args].filter(Boolean), options); }, /** * Execute ReScript `build` command directly * * @param {string[]} [args] * @param {ExecOptions} [options] * @return {Promise} */ execBuild(args = [], options = {}) { return exec(rescript_exe, ["build", ...args], options); }, /** * Execute ReScript `build` command directly and throw on non-zero exit * while preserving captured stdout/stderr for quiet successful tests. * * @param {string[]} [args] * @param {ExecOptions} [options] * @return {Promise} */ async execBuildOrThrow(args = [], options = {}) { const out = await exec(rescript_exe, ["build", ...args], options); if (out.status !== 0) { const err = new Error("ReScript build failed"); err.stack = out.stdout + out.stderr; Object.assign(err, { execResult: out }); throw err; } return out; }, /** * Execute ReScript `clean` command directly * * @param {string[]} [args] * @param {ExecOptions} [options] * @return {Promise} */ execClean(args = [], options = {}) { return exec(rescript_exe, ["clean", ...args], options); }, /** * Execute any binary or wrapper. * It should support Windows as well * * @param {string} bin * @param {string[]} [args] * @param {ExecOptions} [options] * @return {Promise} */ async execBin(bin, args = [], options = {}) { const realPath = await fs.realpath(bin); return exec(realPath, args, options); }, }; }