The art of technology

Blog

Back

CLI application in Node.js

Technology
CLI application in Node.js

It is probably useless to write that JavaScript is a language that is used in other platforms than just in a web browser today. We often come across the fact that we would like our own console application, which performs our necessary activities. Bash, PowerShell or languages such as C, Python, etc. can be used for this purpose. However, why not use something we know. JavaScript can serve us more than well for this purpose.

To make the examples you find here easier to understand, you can take a look at the repository I created for this purpose: apitree-cli-example.

The first step is to install Node.js. At the same time, I recommend using Yarn to run scripts during development. In any case, the choice of whether to use Yarn or NPM is up to you.

After successfully installing Node.js, we can get to work :)

Project preparation

You can set up the project easily using the npm command:

npm init -y

Then we will install the libraries that we will use in our project:

npm i chalk figlet yargs

We will install type definitions of libraries, ts-node and TypeScript into the dev dependencies:

npm i -D @types/node @types/figlet @types/yargs ts-node typescript

Now we will configure TypeScript using the tsconfig.json file (located in the root of the project):

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "lib": ["es6", "es2015", "dom"],
        "declaration": true,
        "outDir": "./lib",
        "rootDir": "./src",
        "strict": true,
        "esModuleInterop": true,
        "resolveJsonModule": true
    },
    "include": ["src/**/*"]
}

Nothing is stopping us now and we can start writing our dream code.

Hello world application

What is better to start with than Hello World.

In the src directory we create an index.ts file with the following code:

console.log('Hello World');

To run our amazing code now, we'll modify the package.json, more precisely blok scripts, as follows:

"scripts": {
  "dev": "ts-node src/index.ts"
}

Since we have installed the ts-node library, we can now run TypeScript files directly without having to compile. So here we go. We will run this in the console:

npm run dev

If you did everything right, the console should list Hello World.

We will probably agree that so far we have not succeeded in anything very amazing. Although we have our first program but writing Hello World in the console is not very useful. That's why we will enrich our project a bit.

Use of the yargs library

You must have noticed that we initially installed different libraries and one of them is called yargs. This library is used to allow us to easily write a program that accepts and validates arguments from the console.

To make our project senseful, let's say we want to write a console application that has two commands: create and update. And the --name parameter is required for the create command. Such an application could serve as a generator of your own applications, such as create-react-app, which many of you certainly know well.

So let's modify index.ts to look like this:

#!/usr/bin/env node
import yargs from 'yargs';
import { ConsoleService } from './service';

const MAIN_CMD = 'at-example';
const TITLE = 'ApiTree CLI';

const log = ConsoleService.getLogger();

enum Command {
    CREATE = 'create',
    UPDATE = 'update',
}

const run = async (command: Command, args: { [appArgs: string]: unknown }) => {
    log.title(await ConsoleService.getTitle(TITLE));

    switch (command) {
        case Command.CREATE:
            log.info(`Create awesome application with name: ${args.name}`);
            return;
        case Command.UPDATE:
            log.info('Update awesome application');
            return;
        default:
            throw new Error(`Command not implement yet`);
    }
};

yargs
    .usage(TITLE)
    .command({
        command: Command.CREATE,
        describe: 'Create awesome application',
        aliases: 'c',
        builder: (builder) => builder.options({ name: { type: 'string', demandOption: true, alias: 'n' } }),
        handler: async (args) => {
            await run(Command.CREATE, args);
        },
    })
    .command({
        command: Command.UPDATE,
        describe: 'Update awesome application',
        aliases: 'u',
        handler: async (args) => {
            await run(Command.UPDATE, args);
        },
    })
    .example(Command.CREATE, `${MAIN_CMD} ${Command.CREATE} --name awesome-app`)
    .example(Command.UPDATE, `${MAIN_CMD} ${Command.UPDATE}`)
    .demandCommand(1, 1)
    .showHelpOnFail(true)
    .epilog('ApiTree software')
    .strict().argv;

That certainly looks more interesting. Now let's go through the individual parts that we see here.

On the first line you may notice (#!/usr/bin/env node). We don't need it at the moment, but when we distribute our console application, it's exactly this line that makes our script a self-running script.

Then we defined all commands that our application will support with Enum type. So, in our case it is create and update.

Then there is the run function, which executes the given code based on the command and args parameter. Here is a link to the method from ConsoleService, which can be found in the mentioned repository.

Then comes the configuration of the yargs library itself. Here are the most interesting commands themselves in the command block. So we need to specify the name of the command, its description, alias, and then the function that executes the code if it is a given command. For the create command, there is also a builder method that returns the option parameter name, which is of type string and is also mandatory. In addition to defining commands, it is good to set other parameters such as demandCommand(min, max). This method tells you how many commands you can enter in a single run. We will stick to the fact that min and max are 1, and therefore it is possible to run only one command.

Now let's test if our application works. We will use the already defined script in package.json for this. We will run the script using Yarn:

yarn dev

If you did everything correctly, the script should end with an error message saying that you must enter a valid argument:

Not enough non-option arguments: got 0, need at least 1

Therefore, let's run the program with the given parameters:

yarn dev create --name awesome-app

We should now get a successful message:

Create awesome application with name: awesome-app

We can also use our aliases:

yarn run dev c -n test-app

That looks a little better. In addition, the yargs library implements help and version arguments. Let's try it:

yarn run dev --help

In that case, we should get a complete listing of the options we have:

ApiTree CLI

Commands:
  index.ts create  Create awesome application                       [aliases: c]
  index.ts update  Update awesome application                       [aliases: u]

Options:
  --help     Show help                                                 [boolean]
  --version  Show version number                                       [boolean]

Examples:
  create  at-example create --name awesome-app
  update  at-example update

ApiTree software

It is also possible to use help on the commands themselves. See:

yarn run dev create --help

Here we can notice that we have added another option, which is marked as required:

  --name, -n                                                 [string] [required]

I think we have our application ready. What exactly and how the application will do is up to you. As a last thing, we will now show you how to actually distribute such an application.

Application distribution

In order to distribute our application, we need to modify our package.json:

"files": [
  "lib/**/*"
],
"main": "./lib/index.js",
"bin": {
  "at-example": "./lib/index.js"
},
"scripts": {
  "dev": "ts-node src/index.ts",
  "build": "tsc"
}

The files block we say which files or directories we will distribute. In our case, it is the lib directory into which the resulting TypeScript file will be compiled.

Next we say which file is the default (main). In this case, it's index.js in the lib directory. This is created by the compilation itself.

Another important setting is the name of the script itself. Here it is good to define a name that is sufficiently descriptive and at the same time does not conflict with other commands in the operating system. In our case, the command will be named at-example.

The last part is the build, which is defined in the scripts block.

Local distribution

Before we distribute our amazing program to the NPM repository, it's a good idea to test it locally. We will use npm for this purpose.

First we compile our project:

npm run build

After compilation, the lib directory is created, in which our project is compiled into JavaScript with a TypeScript definition.

We will now pack our project using NPM to reinstall it globally:

npm pack

After this command, a tgz file will be created in the root of the project, which is named after the name and version in package.json. In my case it is: apitree-apitree-cli-example-0.1.0-alpha.1.tgz, because I have the project name: @ apitree / apitree-cli-example and version: 0.1.0-alpha.1.

We will now install this file globally:

npm i -g apitree-apitree-cli-example-0.1.0-alpha.1.tgz

After a successful installation, we can start our project using the at-example command:

at-example --help

The result is the same as when running via ts-node.

Remote distribution

It is necessary to have an NPM repository for remote distribution. Either on npmjs.com or your own. It is also necessary to log in to this repository. Either using an npm login or via .npmrc file in which an access token is defined.

For remote distribution, just run:

npm publish

And for the installation itself then:

npm i -g @company/my-name

Conclusion

Writing console (CLI) applications in JavaScript is a simple matter. The biggest obstacle here is rather the initial configuration itself, which can discourage anyone. However, if you try this start, you have a plenty of variations on what such an application can do. Rather, you may encounter differences between operating systems. For example, this project was created on MacOS, so to ensure compatibility, the project itself should be tested on other systems as well.