Example Application
Last updated
Last updated
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 are RTA-20
, RTA-21
, and RTA-22
. If omitted, the default algorithm should be RTA-21
.
These options may be provided as command-line arguments or supplied via a configuration file, .spline-reticulator.yml
located in or above the user's current directory.
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:
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.
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.
When using automatic configuration merging, options provided at the command line always take precedence over options provided by the 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.
All JavaScript and TypeScript extensions are supported, and your end-users may write their configuration files using any language features or syntax they choose, so long as their local project's tooling is configured to support those features. For example, Saffron will automatically use a local tsconfig.json
to transpile TypeScript configuration files.
There's a lot going on here, but what matters is:
The user can define their configuration as an async
function.
The user can act based on the contents of a configuration context
(or any function signature you want) provided by our handler
.
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.
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.