Skip to content

Research: CLI third party libs

Michael Hladky edited this page Oct 28, 2023 · 12 revisions

Motivation

The CLI sits in between the user input and the node process. It's main purpose is to interact with the user or the CI pipeline.

A couple of main reasons for a CLI are:

  • process user input
    • validate input
    • transform input
  • prompt information
  • execute logic

This document aims to list a view third party libraries that can be used to create the functionality.

Requirements

Argument parsing and CLI interface

Problems to solve:

  • commands
    • options (like module run -n --force)
  • a dynamically generated help menu based on your arguments,
  • bash-completion shortcuts for commands and options
. Package. Deps Docs. Support NPM installs Bundle Size GitHub Stars OS Support
yargs 7 good recently active 10M+ weekly 110.9kB ~7k. all
commander.js 0 good recently active 15M+ weekly 32.5kB ~20k. all

yargs

yargs helps you build interactive command line tools by parsing arguments and generating an elegant user interface.

commander

commander a similar majored alternative to yargs.

Summary:

Flexibility & Features:
While both libraries offer a robust set of features for command-line parsing, yargs tends to be more feature-rich and offers a more flexible API. It is often praised for its detailed help output and advanced parsing capabilities. Nx uses yargs under the hood and had a re evaluation of the CLI wrapped a year ago and stayed with yargs.

A clear problem with yarg's is the testing and error messages.

Terminal color

Problems to solve:

  • color output
  • weight output
. Package. Deps Docs. Support NPM installs Bundle Size GitHub Stars OS Support
chalk 0 good ??? 20M+ weekly 5.6kB ~15k. all
kleur 0 good ??? 2M+ weekly 1.9kB ~1k. all

chalk.

Uses supports-color to detect whether the current environment supports color, making it very adaptive to different systems and terminals. It also has a graceful fallback for not supported features in different OS.

kleur

As a lightweight alternative, kleur does handle coloring across platforms, but may not have the extensive environment detection that chalk has due to its minimalist approach.

Summary

kleur is juner (maybe more modern stack), but imo the OS support and gracefulness to not existing features wins.

@TODO update for https://github.com/jaywcjlove/colors-cli

Stdout layout & formating

Reporting progress of long running async tasks

As this is essential and not too hard to do I suggest to go with a custom implementation.

Implementation of executeProcess

/**
 * Executes an asynchronous process and returns a `Promise`.
 * It provides a set of callbacks for progress handling (`next`, `error`, `complete`).
 **/
export function executeProcess(cfg: ProcessConfig): Promise<string> {
  const {observer} = cfg;
  let {next, error, complete} = observer || {};
  const nextCb = next || ((v: string) => void 0);
  const errorCb = error || ((v: any) => void 0);
  const completeCb = complete || (() => void 0);

  return new Promise((resolve, reject) => {
    const process = spawn(cfg.command, cfg.args);
    let output = '';

    process.stdout.on('data', (data) => {
      output += data.toString();
      nextCb(data.toString());
    });

    process.stderr.on('data', (data) => {
      output += data.toString();
      nextCb(data.toString());
    });

    process.on('error', (error) => {
      output += error.toString();
      errorCb(error);
      reject(error);
    });

    process.on('close', (code) => {
      if (code === 0) {
        completeCb(output);
        resolve(output);
      } else {
        const error = new Error(`Process exited with code ${code}`);
        errorCb(error);
        reject(error);
      }
    });

  });
}

Usage of executeProcess

const blocking = await executeProcess({
        command: 'node', 
        args: ['./execute-multiple-aidits.js']
    })
    .catchError(() => console.log('execution failed'));

const promise = executeProcess({
        command: 'node', 
        args: ['./execute-multiple-aidits.js'],
        {next: (stdout) => console.log(stdout), error: () => console.log('execution failed')}
     });

Progress Bar

Parsing and validating runner output

As a parser and type generator we use zod.

A downside of zod is, it has hard to read error messages.

Example of the issue:

Error message:

ZodError: [
  {
    "code": "invalid_type",
    "expected": "number",
    "received": "nan",
    "path": [
      "parallel"
    ],
    "message": "Expected number, received nan"
  }
]

A better readable error message could be generated with zod-error.

import { generateErrorMessage, ErrorMessageOptions } from 'zod-error';
import { z } from 'zod';

enum Color {
  Red = 'Red',
  Blue = 'Blue',
}

const options: ErrorMessageOptions = {
  delimiter: {
    error: ' 🔥 ',
  },
  transform: ({ errorMessage, index }) => `Error #${index + 1}: ${errorMessage}`,
};

const schema = z.object({
  color: z.nativeEnum(Color),
  shape: z.string(),
  size: z.number().gt(0),
});

const data = {
  color: 'Green',
  size: -1,
};

const result = schema.safeParse(data);
if (!result.success) {
  const errorMessage = generateErrorMessage(result.error.issues, options);
  throw new Error(errorMessage);
}

Virtual file system

Problems to solve:

  • e2e-test CLI
  • provide dryRun features

List VFS libraries for nodeJs we looked at:

As we want to keep our architecture mostly un-influenced by the VFS we prefere to patch the filesystem, not provide a replacement. vinyl, unionfs, memory-fs are not patching so we exclude them.

. Package. Deps Docs. Support NPM installs Bundle Size GitHub Stars OS Support
fs-monkey lots todo ??? 12M+ weekly 8.4kB ~100 all
mock-fs lots todo ??? 651k+ weekly 27.1kB ~900 all

Testing in vitest:
https://github.com/flowup/quality-metrics-cli/commit/3aa29409a45536e258d05ea3e37d693a3c403fe5