Example Application
Let's imagine we are building a command-line interface for a Node application that will help us reticulate splines. The CLI has the following requirements:
It must be provided 1 positional argument,
spline, indicating the spline to be reticulated.It may be provided 1 optional named argument,
algorithm, indicating the reticulation algorithm to use. Valid algorithms areRTA-20,RTA-21, andRTA-22. If omitted, the default algorithm should beRTA-21.These options may be provided as command-line arguments or supplied via a configuration file,
.spline-reticulator.ymllocated in or above the user's current directory.
Part 1: CLI Implementation
Let's build-out a quick CLI for this application to make sure options/arguments are being processed per the above requirements.
First, we should have a well-formed package.json in our project. The name, version, and description fields will be read by Saffron to set sensible defaults and to report the current version of your application when the --version flag is passed. Additionally, the bin field should point to the file that Node should use as the entry-point to your application:
{
"name": "@fluffykins/spline-reticulator",
"version": "1.2.3",
"description": "Reticulates splines using various algorithms.",
"files": [
"dist"
],
"bin": {
"spline-reticulator": "dist/bin/cli.js"
},
"dependencies": {
"@darkobits/saffron": "^X.Y.Z"
}
}Because our example package name is scoped, we'll be using the object form for bin to ensure that when this package is installed by the end-user, NPM creates a symlink named spline-reticulator. If we used a string, NPM would instead use the full package name. If your application does not use a scoped name, you can use the string form to declare bin.
See the NPM documentation on bin for more information.
Next, we can define our command-line interface:
First, lets try invoking spline-reticulator --help to make sure our usage instructions look good. Note that if your project requires a build step, you may need to invoke your CLI with a command like node ./dist/bin/cli.js --help.
Note the name and description used above was derived from the name (minus any scope) and description fields of our application's package.json. These are sensible defaults, but can be customized by calling command.scriptName() from our builder and by providing a description property in our command definition, respectively:
We can verify that our CLI works as expected by calling it in various ways, and ensuring it uses a configuration file when present:
Because this command passed Yargs' validation, our handler was invoked, which logged the parsed arguments object. Let's provide an invalid algorithm and ensure that Yargs catches the mistake:
Notice the Invalid values: section at the end, indicating the erroneous reticulation algorithm.
Part 2: Basic Configuration File
Let's try adding a configuration file and ensure that Saffron loads it correctly. By default, Saffron will use the un-scoped portion of the name field from your project's package.json. Since our package is named @fluffykins/spline-reticulator, Saffron will use spline-reticulator as the base name when searching for configuration files. One of the default supported configuration file types would thus be .spline-reticulator.yml. If this file is found in or above the directory from which we invoked our application, it would be loaded and merged with any arguments provided.
Notice we did not provide an --algorithm argument, and the default algorithm RTA-21 has been superseded by the one we provided in our configuration file, RTA-22. Merging configuration and command-line arguments (auto-configuration) can be disabled if it does not suit your application's requirements. See Part 3 below for a demonstration.
Part 3: JavaScript Configuration File
To demonstrate more of Saffron's capabilities, let's add some additional requirements to our application:
The end-user may need to fetch the spline to reticulate from a remote server.
Our application may pass some context to the user to help them define their configuration.
To satisfy these requirements, we can allow users to configure our application using a JavaScript (or TypeScript) configuration file. Using the same base name from package.json, Saffron (via Cosmiconfig) will search for a file named spline-reticulator.config.ts at or above the user's current directory.
There's a lot going on here, but what matters is:
The user can define their configuration as an
asyncfunction.The user can act based on the contents of a configuration
context(or any function signature you want) provided by ourhandler.
Now, let's see what modifications we need to make to our CLI's implementation to support this use-case:
And that's it! ✨
We variablized our list of supported algorithms so there is a single source of truth for the CLI and the context object we pass to configuration functions.
We disabled auto-configuration to support our more advanced use-case.
We added support for invoking and
await-ing configuration functions that will be passed a list of our supported algorithms.Despite all these changes, we retained the ability for users to continue to use a basic YAML configuration file if they so desire.
Wrapping Up
Typically, by the time an application has grown sophisticated enough to require configuration files that involve asynchronous functions, its command-line arguments and configuration schema will have little to no overlap, and Saffron's auto-merging feature will likely remain disabled. This also means that Yargs will no longer be validating our configuration. You could, however, use a tool like deepmerge to recursively merge argv and resolvedConfig into a single object. Validation libraries like ow or ajv could also be used to ensure our resolved configuration satisfies a particular schema.
To learn more about how to use Saffron in detail, see the API Reference.
Last updated