Getting started with Clipanion, the CLI library that powers Yarn Modern

xsmith
The Startup
Published in
6 min readJun 30, 2020

Clipanion takes everything you love, or hate, about making CLI applications and throws it out the door. And that’s a good thing!

Photo by Todd Quackenbush on Unsplash

Clipanion was created by Maël Nison aka Arcanis. It, like others, is a library for building Command Line Interface (CLI) applications, but what sets it apart from the others is that it’s not just your average CLIbrary. But why is that?

A bit about other CLIbraries

If you’ve wanted to develop a program like Yarn, npm, Rails, or any other similar terminal applications in Node.js you’ve probably dealt with the three major libraries, Commander, Minimist, and Yargs which are all respectfully damn good libraries. What they fail to do is really expand on the idea of developing larger more complicated programs. In fact, they’re pretty simplistic. Of course, this isn’t that big of a problem since Commander powers Yarn Classic and has for a long time.

But if you’re like me and peeked under-the-hood, you’d have realized just how painful it was to manage such a large application. Commander by far is the top tool in getting started quickly, I myself have used it a number of times, to be honest. And over time I started to grow a severe distaste for these libraries and I wasn’t alone. Powerful as they are when you want to develop a class-based program they don’t bode well, often sticking out like a sore thumb. But what specifically sticks out? Let’s look at an example of Commander.

// Command implemented using action handler (description is supplied separately to `.command`)
// Returns new command for configuring.
program
.command('clone <source> [destination]')
.description('clone a repository into a newly created directory')
.action((source, destination) => {
console.log('clone command called');
});

// Command implemented using stand-alone executable file (description is second parameter to `.command`)
// Returns `this` for adding more commands.
program
.command('start <service>', 'start named service')
.command('stop [service]', 'stop named service, or all if no name supplied');

// Command prepared separately.
// Returns `this` for adding more commands.
program
.addCommand(build.makeBuildCommand());

One thing you’ll notice is the biggest issue a lot of developers hate, the overuse of method chaining. It’s clunky. Sure, when you have a smaller application it doesn’t mean much, but when you have a large application like Yarn Classic all you see is a wall of method chains. It's just ugly, but ugly isn’t a real complaint, gentlemen. Another complaint has always been the lack of support for sub-commands. For example yarn global add gatsby-cli looks simple enough but the implementation is over complicated.

I won’t complain too much, if you’ve used these before then we all have our individual headaches. However, one thing to note that we can all agree on is performance. This has been a long debate between using Yarn or npm, which is better, which is lighter, but most importantly which is faster. For a long time, it was Yarn, but occasionally even Yarn didn’t feel up-to-speed. This comes down to the BTS of how each CLIbrary functions. Many use a simple recursion loop when parsing commands and input. Commander, I’m talking about you. But Clipanion breaks away from the standard implementation in a grand way, so let’s talk about that.

Introducing, Clipanion. Your dependency-free Type-safe CLI companion

Clipanion, and I do hate to be repetitive here, changes the game in almost every possible approach. Funny enough though it used to be almost the same, I reached out to Arcanis to thank him for creating the CLIbrary because actually had the same idea he did, almost 1x1 except he beat me to the implementation with the Finite State Machine, and he told me originally it almost looked completely different. And the man wasn’t lying!

Thanks, I’m glad to hear that! The original version of Clipanion was quite different, but over time I noticed it was starting to become quite a beast and wouldn’t be maintainable on the long run. I believe the current model is much saner 😃

Basically what sets apart this CLIbrary from the others is… everything. It’s not even a real competition. Sure, Yargs and others have unique traits but I don’t think Arcanis cares about competition, like myself he just enjoys making tools to help solve problems and make development easier and more performant. Mad respect!

Clipanion is built completely in TypeScript and uses a Finite State Machine to resolve and conduct commands in a fast and efficient way all the while using a class-based approach to kill the method chaining. He doesn’t argue it faster, but it sure as hell is. So let’s look at how to use it, btw I use Yarn Modern and so does Clipanion.

yarn add clipanion

Obviously you have to install it, but I figured some people might like to see the command, I don’t know why. Getting started after installation is straightforward. I’m using TypeScript, as should you to really take advantage of Clipanion and all it has to offer.

This is a very basic setup, completely easy to follow. For more information, you can refer to the repository which is where you can read the minimal documentation.

The first thing you’ll need to do is enable the experimental decorators in your tsconfig.json, although there is a fallback syntax the decorators are more straightforward. All we have to do to create commands is make sure our command class extends Command<CommandContext>, which is arbitrary because there’s a built-in Cli.defaultContext which just uses std, we can name and customize our CommandContext any way we want. Keeping this example simplified I only use @Command.Path('command') which is a simple way to tell Clipanion what string to listen for, and you can add any number of these!

@Command.Path('hello') // We listen for "generate"
@Command.Path('h') // We listen for alias "g"
@Command.Path() // We set this as the default listener
async execute(): Promise<void> {
this.contect.stdout.write("Hello, World!");
}

I originally didn’t know how to set up command aliases but I read a smaller sentence where Arcanis said you can add any number of paths. So, what about sub-commands? If we look at Yarn Modern they have yarn workspaces list which adds a package in a specific workspace. That’s easy, Clipanion allows us to setup sub-commands as naturally as any other command.

export default class WorkspacesListCommand extends BaseCommand {
@Command.Boolean(`-v,--verbose`)
verbose: boolean = false;

@Command.Path(`workspaces`, `list`)
async execute() {
...
}
}

Supernatural! I also added @Command.Boolean('option') which as you can see allows us to listen for a string that’s interpreted as boolean. @Command.String('option', { tolerateBoolean: true}) allows us to listen for a string, but tolerateBoolean gives it the same treatment as the boolean. Finally, there’s @Command.Array() which accept an array of strings, such as an array of package names for yarn to install if you need an example.

It goes without saying, and without extensive examples, that Clipanion is a game-changer for developing CLI applications. I’m using it myself to develop the toolkit for the Yarnberry Cookbook.

A large number of commands

This worried me too for a bit, but I dug into Yarn Modern’s code and found a reasonable solution. Basically if you export default CommandName you have the opportunity to recursively import and register the commands. The way I solved this, taking Yarn’s code in mind, was to put every command in a commands/ and put BaseCommand in tools/ so that we don’t import it on accident which breaks things. Then you just want to make sure you didn’t hardcode any command registrations like cli.register(WelcomeCommand), you’ll want to remove that.

This is how I set mine up, using some of Yarn Modern’s logic altered to fit a folder instead of packages and plugins. I’m no CS grad, I’m just a kinda smart self-taught developer. So sorry if I have any bad habits. Otherwise, I think you should check out Clipanion for yourself!

In Summary

Thank you, Maël Nison, for one hell of a package CLIbrary.

--

--

xsmith
The Startup

Full Stack Developer. Started coding at 16 and haven't stopped. Always looking to explore new technologies and write about others.