6

Writing an Extensible CLI Tool in 2022 with Node.js

 1 year ago
source link: https://blog.bitsrc.io/writing-an-extensible-cli-tool-in-2022-with-node-js-43eb03150be2
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Writing an Extensible CLI Tool in 2022 with Node.js

Making one is easy, making it so it can be extended without shooting yourself in the foot? That’s not so straightforward

Photo by Rohit Farmer on Unsplash

Writing a CLI (Command Line Interface) tool in 2022 using Node.js is no longer a real challenge since there are countless packages already that help you do just that.

However, writing one that is easily extensible is can be updated easily is another story. After all, these “features” aren’t necessarily provided by libraries out there, instead, you need to apply good practices and some design pattern magic.

This is why today I’m going to show you just that, the magic you need to keep things growing and easily maintainable within your project. Feel free to take these learnings and apply them to whatever project you’re working on.

What are we making then?

I’m not going to overcomplicate the build here, what I’m trying to create is a simple CLI tool that allows us to perform certain tasks and has room to be extended with further functionality.

In particular, the project I built provides 3 commands to deal with strings:

  • You can ask it to split a string into words, or overwrite the default behavior and specify another separator.
  • You can ask it to uppercase a string. Simple, nothing fancy.
  • And you can also ask it to count how many words are there on a string. Or if you want to override the default behavior, you can count how many times a single word (specified by you) appears in the string.

Now since we’re doing this intending to publish our tool, we also want to provide all the help text, and parameter validation we can think of. So things like descriptions, required and optional attributes, and so on, will be included.

Here is a screenshot of our tool working, displaying the default help screen:

Notice the 3 commands (split, upper, and wc) and the extra “help” command that will provide details for each of the other ones.

The tools we’ll use

This is not going to be an overcomplicated build, as I mentioned at the beginning of the article, it’s 2022, there are plenty of libraries out there that we can use. And in particular, I’m going to be using the Commander library, which provides everything I needed for this.

Of course, this is going to be built using Node.js with plain JavaScript, but you can definitely add TS or any other extra layer you’d like. I’m just going to show you the basics.

What made me go with commander? Two things:

  1. Its set of features. As we’re going to be seeing, adding commands, attributes, and their implementation is quite straightforward. It requires zero boilerplate.
  2. It seems to be quite popular and actively maintained. According to NPM, it gets downloaded over 86 million times per week and it’s been updated in the last two months (at the time of writing this). So it checks all the boxes I need for me to trust in it.

Finally, the last “tool” I’m going to be using is not necessarily a piece of code, but a design pattern, one, in fact, of my favorite ones: the, aptly named, command pattern. Let’s quickly take a look at it so you can understand why I’m using it.

Understanding the Command pattern

Just in case you don’t know what design patterns are, they’re just blueprints you can use to understand how to best architect the internal structure of your project (I’m talking about things like which classes to implement and how to make them interact with each other here). Each pattern will provide certain benefits like re-usability, modularization, maintainability, reactivity, etc.

In particular for our use case, I was after modularization and extensibility, so let’s take a look at the Command pattern and see if we can get what we need out of it.

In a nutshell, this pattern tells you that you should break up your business logic into a set of self-contained “capsules” that can be sent to execute without the need for context or any other information or links to the outside. This is a great pattern to use when you have actions that need to be executed and one of two things (or both) can happen:

  1. These actions can be executed from multiple places of your UI or through different commands. This way the logic for the action itself is centralized inside the capsule and you don’t have to duplicate code.
  2. You’ll need to keep adding new actions in the future and you want that process to have as little of an impact on the rest of your code as possible. Ideally just adding a new independent capsule into your project would be enough.

If you’re more of a visual thinker like I am, here is a basic UML diagram showing the relationships between the involved classes:

The “client” class is a fake consumer of our commands. Ideally on your project that would be replaced by any code invoking the commands. The main idea behind this structure, is that:

  1. All commands have the same “shape”, in the sense that they all implement the same interface (if you have such a construct, like with TS) and they extend the same general class. This last point is all about code reusability, all commands will have similar behaviors in the sense that they can be executed, they will probably generate some sort of output and structure-wise, they will most likely share the same basic characteristics (i.e in our case, they will all have a name, a description, a set of attributes and options).
  2. The “client” code executing the command doesn’t need to know exactly which command it is executing. All of them should be callable from the same place, and that place can be anywhere inside the client code. This is what isolates these “capsules” I mentioned earlier from the context around them.

Implementation-wise, we’ll see that this is quite simple, especially in JS where there are no types to worry about. However, if you were working with TS, all you’d have to do is implement an interface for the Command class to implement and then make sure your client code imports that interfaces as the type of each command.

Anyway, that’s enough theory for this article, let’s take a look at the actual implementation of this command so we can understand how it’s actually going to help us.

If you liked what you’ve read so far, consider subscribing to my FREE newsletter “The rambling of an old developer” and get regular advice about the IT industry directly in your inbox.

Implementing the client code first

Something I really like doing is to start implementing things with a top-down approach. Meaning, I will first implement the client code assuming I already have my “capsules” ready and following the pattern to perfection, and that will help me “see” the API I need them to implement in the future.

The point of doing this approach is to avoid having any type of bias you might have while implementing the command pattern from leaking into your client-side code. Ideally, if you’re building this as part of a team, your commands should make the client-side’s life a lot easier, not the other way around. So focus on that and implement that part first. Of course, the client-side will assume a command-like pattern is used for the capsules, that will be the only influence we’ll allow on that design.

So let’s take a look and see what I mean:

This is all the client code we’ll need. So let’s break it up into sections:

  1. The first thing we do is import both, the Commander package, and our list of commands (we’ll take a look at this one in a minute).
  2. Then we iterate over our list of commands, and for each one we define a command for Commander to understand. This is how the library works, you define the command, and then have it parse the command line arguments to define which one needs to be executed.
  3. We then parse the input and decide which command’s action method to call. This is done in the last line of code, by calling commander’s parse method.

This is the command pattern in all its glory. We iterated over a list of commands as if they were the exact same object (and they’re not as we’ll see in a minute). For each one we asked them for the same attributes, and none of them complain about it. Finally, we defined the execution of their business logic for each one (lines 23–26) calling their action method, and guess what? None of them said, “wait, no, I don’t know what method you’re talking about”. This is because they all share the same “shape”, or in other words, they all extend the same common class.

Let’s now take a look at this common class, which as you’re about to see, has no real magic inside:

It’s basically a dummy class. It defines which attributes all commands will have and then it’ll define the action method that all of them have to implement. If we had access to abstract methods or classes in JS, this one would be a perfect candidate for it, since you would never directly instantiate the Command class, it is just there to make sure all commands follow the same standards.

And by that, I mean they should all, at least, implement the action method. If they needed extra methods for their specific business logic, they should all be private ones. But alas, this is JS, the land of the public, at least for now until the private operator gets released.

So a usual command would look like this:

Now, the key places to look at first are:

  1. The constructor. As you can see, our command does not accept any parameters, instead it pre-defines them on its own. Meaning, this command is independent of its context, and anyone calling it will have the same result because it already knows exactly how to define itself.
  2. The action method. It defines the business logic for our command. It is meant to overwrite the one defined on the Command class entirely, which is why we’re not calling the other one through super or anything. And it’s always going to receive 2 attributes, the string we’re dealing with, and the options specified by the user. This is standard and defined by Commander, so we can trust it. However, if we wanted to be even more isolated from the outside world, we could just expect a “payload” object, and have each command deal with it however they needed.
  3. Finally, the two auxiliary methods, _countWords and _countSingleWord are here representing “private” methods. The part of the command that only gets used internally and ideally, would not be accessible from the outside world.

If you want to see how the other (simpler) commands were implemented, here is the full repo for you to play with.

Making it easily extensible

One last little detail about the implementation and the main reason why we also wanted to use the command pattern, is that I wanted this project to be easily extended.

We can already see that if we wanted to change the business logic behind our commands, all we’d have to do is to edit a single file. Just modify the “capsule” for the one we need, and we’re done. That is major.

However, what happens if we need to ADD a new command? Back on the client-code snippet, you can see we’re importing the list of commands from the “commands” folder. But how are we collecting said list? Is it hard-coded or dynamic?

A hard-coded list would not be the worst thing possible, but it would imply that we also need to modify that file when we add a new command. And c’mon, I’m sure we can do better than that, can’t we?

Here is what the index.js file inside the commands folder looks like:

As you can see, all we’re doing is we’re going through all the files inside the folder (the commands folder) and if they end with “Command.js”, then we’re importing them and instantiating them.

We then export the list of instances of our commands, so the main loop can take them and use them as we already saw.

This allows us to add new commands seemingly by dropping them inside the right folder, as long as they have the right file name.

Magic!

As you can see, the process of implementing a CLI tool is quite simple to be honest, there are plenty of tools already that help with the task. The point here is to use the right design pattern for your use case.

In our fake scenario, we had the need to make this project easily extensible when it came to dealing with the commands. Making sure modifying them and adding new ones was easy was the main objective. And I think we managed to do so, wouldn’t you agree?

Have you used the command pattern in the past? What about any other design pattern? What’s your favorite one? Share it in the comments and I’ll share mine!

Build composable web applications

Don’t build web monoliths. Use Bit to create and compose decoupled software components — in your favourite frameworks like React or Node. Build scalable and modular applications with a powerful and enjoyable dev experience.

Bring your team to Bit Cloud to host and collaborate on components together, and speed up, scale, and standardize development as a team. Try composable frontends with a Design System or Micro Frontends, or explore the composable backend with serverside components.

Give it a try →

Learn more


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK