Further improvements to the execa library

28 Sep, 2021Node.js

The execa library offers a more ergonomic way to execute external commands on top of the Node.js's child_process module. But I found few options I always want to have particularly when developing Node.js CLI scripts.

Options

printCommand

When running scripts in CI environments like GitHub Actions or GitLab CI, it prints the currently executing commands in the console (usually with a > or $ at the start of the line to simulate the shell prompt), so when an error occurs, you can easily tell what went wrong. This options can be implemented with:

if (options.printCommand !== false) {
  process.stdout.write(`\n> ${script} ${args.join(' ')}\n`);
}

forceColor

Modern CI environments widely support colors in the output consoles, but some libraries disable the colors when it detects that process.env.CI is set or process.stdout.isTTY is false, aiming to avoid showing the ANSI escape code that looks like \e[0;32m in the output that might obstruct people reading the logs.

But we can force the libraries to use colors by setting the FORCE_COLOR env variable to 1 (or 2 or 3, refer to https://nodejs.org/docs/latest/api/tty.html#writestreamgetcolordepthenv), to make the output in the CI environment more readable.

if (options.forceColor !== false) {
  options.env = { ...process.env, FORCE_COLOR: '1' };
}

silent

execa by default does not print the output of the executed command to the console, but for the same reason as printCommand, it's more human-friendly to print the output by default, and disable it with the silent option set to be true for potential verbose commands (like curl) or when the output is meant to be parsed programmatically.

if (options.silent !== true) {
  task.stdout?.pipe(process.stdout);
  task.stderr?.pipe(process.stderr);
}

exit

execa does not always trow an error when the command fails, but when running in the CI environment, it's better to exit the process with a non-zero exit code so the pipeline can be terminated early.

if (options.exit !== false) {
  process.stderr.write(`\n${error.stderr}\n`);
  process.exit(error.exitCode);
}

Usage examples

1. Fetching a JSON file with curl:

const { stdout } = await exec('curl', ['-fsS', '-m5', url], {
  silent: true,
  exit: true
});
const data = JSON.parse(stdout);

2. A function to tell if the target git directory is clean:

async function isDirClean(dir?: string) {
  const { stdout } = await exec('git', ['status', '--porcelain'], {
    cwd: dir,
    printCommand: false,
    silent: true
  });
  return stdout.trim() === '';
}

Complete code

import execa, { Options as ExecaOptions, ExecaError } from 'execa';

interface Options extends ExecaOptions {
  printCommand?: boolean;
  forceColor?: boolean;
  silent?: boolean;
  exit?: boolean;
}

export default async function exec(
  script: string,
  args: string[] = [],
  opts: Options = {},
) {
  const options = { ...opts };

  if (options.printCommand !== false) {
    process.stdout.write(`\n> ${script} ${args.join(' ')}\n`);
  }

  if (options.forceColor !== false) {
    options.env = { ...process.env, FORCE_COLOR: '1' };
  }

  const task = execa(script, args, options);

  if (options.silent !== true) {
    task.stdout?.pipe(process.stdout);
    task.stderr?.pipe(process.stderr);
  }

  try {
    return await task;
  } catch (err) {
    const error = err as ExecaError;
    if (options.exit !== false) {
      process.stderr.write(`\n${error.stderr}\n`);
      process.exit(error.exitCode);
    } else {
      return error;
    }
  }
}

Powered by Gatsby. Theme inspired by end2end.

© 2014-2022. Made withby mdluo.