Source: utils/cfgi-runner.js

import { execSync, spawnSync } from "child_process";
import chalk from "chalk";
import ora from "ora";
const pink = chalk.hex("#FFC0CB");
/**
 * @fileOverview Defines all the logic for the cfgi runner.
 * @author Gerard Hernandez
 * @module cfgi/runner
 *
 * @requires     {@link https://www.npmjs.com/package/chalk | chalk}
 * @requires     {@link https://www.npmjs.com/package/ora | ora}
 * @requires     {@link https://nodejs.org/api/process.html | process}
 *
 * @exports commandLive
 * @exports command
 * @exports runs
 * @exports task
 * @exports TaskConfig
 * @exports RunOutput
 * @exports RunError
 *
 */
process.on("SIGINT", () => {
    console.log("");
});
/**
 * Executes a command synchronously and captures live output.
 * @function
 * @param {string} cmd - The command to be executed.
 * @param {boolean} [silent=false] - If true, suppresses output; otherwise, displays live output.
 * @returns {RunOutput} A success message if the command is executed successfully, or nothing if there's an error.
 * @throws {Error} Throws an error if the command is not provided or not correctly formatted, or if an error occurs during execution.
 * @example
 * commandLive('pnpm next dev'); //=> { output: 'Command exited successfully.', silent: false, isError: false }
 * @example
 * commandLive('pnpm next dev', true); //=> { output: '', silent: true, isError: false }
 */
export function commandLive(cmd, silent = false) {
    if (!cmd) {
        throw new Error("No command is provided.");
    }
    const parts = cmd.split(/\s/);
    const mainCmd = parts[0];
    const args = parts.slice(1);
    if (!mainCmd) {
        throw new Error("Command not correctly formatted.");
    }
    const options = {
        stdio: silent ? "ignore" : "inherit",
    };
    const childProcess = spawnSync(mainCmd, args, options);
    if (childProcess.error) {
        throw childProcess.error;
    }
    if (childProcess.status === 0) {
        return { output: "Command exited successfully.", silent, isError: false };
    }
    else {
        return { output: "Command exited.", silent, isError: true };
    }
}
/**
 * Executes a command synchronously using the specified command string.
 * @function
 * @param {string} cmd - The command string to be executed.
 * @param {boolean} [silent=true] - Whether to suppress output.
 * @returns {RunOutput} An object containing information about the command execution.
 * @example
 * command('pnpm prettier --write .'); //=> { output: '', silent: true, isError: false }
 * @example
 * command('pnpm prettier --write .', false); //=> { output: '...', silent: false, isError: false }
 */
export function command(cmd, silent = true) {
    try {
        const stdout = execSync(cmd);
        const output = stdout.toString();
        return { output: output, silent, isError: false };
    }
    catch (error) {
        return { output: "", silent, isError: true };
    }
}
/**
 * Represents a run within a task.
 * A run must always have a return statement within its function body.
 * If it does not have one it will be added automatically.
 * @function
 * @param {string} name - The name of the run.
 * @param {function|RunOutput} runFunction - The function that defines the run's behavior.
 * @returns {RunsReturn} An object representing the run.
 * @example
 * runs("a passing command", () => {
 *  command("exit 0");
 * });
 * */
export function runs(name, runFunction) {
    let functionString = runFunction.toString();
    const commandRegex = /(command|commandLive)\((.*)\);/g;
    const returnRegex = /return\s+(command|commandLive)\((.*)\);/g;
    // check if there are any return statements in the function
    const returnMatches = functionString.match(returnRegex);
    if (returnMatches) {
        return {
            name: name,
            run: runFunction,
        };
    }
    // check if there are any command statements in the function
    const matches = functionString.match(commandRegex);
    if (!matches) {
        return {
            name: name,
            run: runFunction,
        };
    }
    // if there are command statements, find the last one
    const lastMatch = matches[matches.length - 1];
    if (!lastMatch)
        return {
            name: name,
            run: runFunction,
        };
    const lastMatchIndex = functionString.lastIndexOf(lastMatch);
    // replace the last command statement with a return statement
    functionString =
        functionString.substring(0, lastMatchIndex) +
            `return ${lastMatch}` +
            functionString.substring(lastMatchIndex + lastMatch.length);
    // return the new function
    return { name: name, run: eval(functionString) };
}
/**
 * Represents a task with a setup function and a list of runs.
 * @function
 * @param {string} name - The name of the task.
 * @param {function} setup - The setup function to be executed before runs.
 * @param {{ name: string, run: function() }} runs - An array of runs, each containing a name and a run function.
 * @param {TaskConfig} [config] - An optional configuration object for the task.
 * @returns {{passed: number, errors: number}} - The number of passed and failed runs.
 * @example
 * task(
 *   "a task", // name
 *   () => { // setup
 *     command("exit 0");
 *   },
 *   [ // runs
 *     runs("a passing command", () => {
 *       command("exit 0");
 *     }),
 *   ]
 * );
 *
 */
export function task(name, setup, runs, config) {
    if (setup)
        setup();
    const startTime = new Date().getTime();
    const typedRuns = runs.map((r) => {
        return {
            name: r.name,
            run: r.run,
            type: r.run.toString().indexOf("commandLive") > -1 ? "live" : "sync",
            forcedSilent: config?.silent || false,
        };
    });
    const syncRuns = typedRuns.filter((r) => r.type === "sync");
    const liveRuns = typedRuns.filter((r) => r.type === "live");
    const { successful: successfulSync, errors: errorsSync } = runSyncRuns(syncRuns, config?.exclude);
    const { successful: successfulLive, errors: errorsLive } = runLiveRuns(liveRuns, config?.exclude);
    const successful = successfulSync.concat(successfulLive);
    const errors = errorsSync.concat(errorsLive);
    const endTime = new Date().getTime();
    cleanup(name, successful, errors, config?.silent, endTime - startTime);
    return { passed: successful.length, errors: errors.length };
}
function runSyncRuns(syncRuns, exclude) {
    if (exclude && exclude === "sync") {
        console.log("");
        console.log(chalk.yellow(`ℹ Skipping sync tasks.`));
        console.log("");
        return { successful: [], errors: [] };
    }
    const successful = [];
    const errors = [];
    console.log(chalk.yellow(`ℹ Running regular tasks:\n`));
    syncRuns.forEach((r) => {
        const spinner = ora(`Running ${pink(command.name)}...`).start();
        const { output, silent, isError } = r.run();
        if (isError) {
            spinner.stopAndPersist({
                symbol: chalk.red("✖"),
                text: `Task ${pink(r.name)} failed.`,
            });
            errors.push({ name: r.name, output: output || "Unknown error" });
        }
        else {
            spinner.stopAndPersist({
                symbol: chalk.green("✔"),
                text: `Task ${pink(r.name)} ran successfully.`,
            });
            !silent &&
                !r.forcedSilent &&
                console.log(`  ${chalk.blue("└→")} ${output}`);
            successful.push(r.name);
        }
    });
    return { successful, errors };
}
function runLiveRuns(liveRuns, exclude) {
    const successful = [];
    const errors = [];
    if (exclude && exclude === "live") {
        console.log("");
        console.log(chalk.yellow(`ℹ Skipping live tasks.`));
        console.log("");
        return { successful: [], errors: [] };
    }
    liveRuns.forEach((r) => {
        console.log(chalk.yellow(`\nℹ Running live command ${pink(r.name)} as a child process:\n`));
        const { output, silent, isError } = r.run();
        if (isError) {
            console.log(chalk.red(`✖ Task ${pink(r.name)} failed.`));
            errors.push({ name: r.name, output: output || "Unknown error" });
            return;
        }
        if (!silent) {
            console.log(`  ${chalk.blue("└→")} ${output}`);
        }
        successful.push(r.name);
    });
    return { successful, errors };
}
function handleErrors(errors) {
    console.log();
    console.log(chalk.red(`✖ ${errors.length} tasks failed ro run!`));
    errors.slice(0, -1).forEach((e) => {
        console.log(`  ${chalk.red("|→")} ${chalk.blue(e.name)} ${e.output && `${chalk.red("→")} ${e.output}`} `);
    });
    errors[0] &&
        console.log(`  ${chalk.red("└→")} ${chalk.blue(errors[0].name)} ${errors[0].output && `${chalk.red("→")} ${errors[0].output}`} `);
}
function cleanup(taskName, successful, errors, silent, totalTime) {
    if (!silent) {
        console.log(chalk.yellow(`\nℹ Summary:\n`));
        if (successful && successful.length > 0)
            console.log(chalk.green(`✔ ${successful.length} tasks ran successfully.`));
        if (errors && errors.length > 0)
            handleErrors(errors);
        console.log("");
        console.log(chalk.yellow(`ℹ Completed task ${pink(taskName)} in ${totalTime / 1000}s`));
        return;
    }
    else {
        console.log("");
        console.log(chalk.yellow(`ℹ Completed task ${pink(taskName)} in ${totalTime / 1000}s`));
        console.log("");
    }
}