Skip to content

Instantly share code, notes, and snippets.

@yowainwright
Last active December 21, 2022 19:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yowainwright/ba8164ad5d968f35ae86e2ba6c91c592 to your computer and use it in GitHub Desktop.
Save yowainwright/ba8164ad5d968f35ae86e2ba6c91c592 to your computer and use it in GitHub Desktop.
A Node CLI program example

Node CLI Program to Execute Standalone Node Script

The following file(s) aim to display a pattern of node script development.
In this pattern the CLI program is built to execute the standalone node script.

Writing Node Scripts

  1. It is a common pattern to write a script.
  2. It is a common pattern to write a CLI program to run a script.

Desires

  1. I want to easily write CLI programs to execute a node script
  2. I want that CLI program to be easily unit tested based on its options, not the script it runs! 🎉

Directory Structure

Directory structure of the script

flowchart LR
A(src/);
A --> B(program.ts: cli script runner);
A --> C(script.ts: node script);
A --> D(tests/);
D --> E(program.test.ts: tests program);
D --> F(script.test.ts: tests script);

Developer Ergonomics

Options to execute the script

flowchart LR
A(node script);
A --> B(execute script via CLI);
A --> C(execute script via LAMBDA Handler);
A --> D(execute script in other node application);

Summary

The charts aboves diagrams a node script which works on it's own but can be executed in multiple formats.

The example-program.ts file below displays how the CLI portion of Developer Ergonomics (Ref 2nd diagram) can execute the script and provide options to easily unit test it.

See stdoutToJSON for more detail.

#!/usr/bin/env node
const { program } = require("commander");
const { cosmiconfigSync } = require("cosmiconfig");
const { script } = require("./script");
import { Options } from "./types";
const version = "VERSION";
/**
* Below is an example of a Node CLI program.
* The program's action function accepts an "options" argument,
* which informs the function's behavior.
* Code comments within the action function specifiy how unit tests
* can be written to ensure that each option is respected correctly.
* --
* Directory structure:
* src/
* __tests__
* - program.test.ts // tests the CLI program options argument unrelated to the script
* - script.test.ts // tests the script unrelated to the CLI program
* - program.ts
* - script.ts
*/
// used to grab a config file for the Node CLI program to read from
const explorer = cosmiconfigSync("config");
/**
* action
* @param {Name} string
* @param {Org} string
* @param {Options} options
*/
export function action(name, org, options: Options = {}): void {
/**
* our CLI unit tests should now test the config and that it can be accessed via an option or via cosmiconfig (a config finder)
* if option.config is defined, that should take presedence
* if option.config is defined, it should have x, y, z contents
* if defaultConfig is defined (via cosmiconfig), it should not be an empty object
* if the defaultConfig is defined, it should x,y,z contents
*/
const { config: defaultConfig = {} } = options?.config
? explorer.load(options.config)
: explorer.search() || {};
/**
* our CLI unit tests should now test which urls are loaded
* if option.url is defined, that should take presedence over the defaultConfig.urls or the URLS constant
* if defaultConfig.url is defined, that should take presedence over the URLS constant
*/
const urls = options?.urls || defaultConfig?.urls || URLS;
/**
* by exiting via the options.isTestingCLI,
* we should now test the options as listed in the scenerios above
*/
if (options.isTestingCLI) {
/**
* the console.info log provides a mechanism
* to print stdout (standard output) to the terminal for executed child process
* a tool called stdoutJSON (soon stdoutToJSON) can be used to convert the stdout back the parsable JSON
* https://github.com/yowainwright/stdoutJSON
*/
console.info({ options, urls, cookies, segmentRequest });
return;
}
/**
* The script function is tested seperately in separate unit tests in a separate file
* AKA the UI test scenerios above should only test the CLI program's options and config
*/
script({ options, name, org });
}
program
.version(version)
.description("tests cli")
.option("-u, --urls [urls...]", "urls to run scripts on")
.option("-c, --config <config>", "config file to use")
.option("-t, --isTestingCLI", "enables CLI testing, no scripts are run")
.action(action)
.parse(process.argv);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment