Javascript CLI demystified
Tags: javascript, tutorial, beginners
Published: 2022-08-04 19:13:21

The other day, I was adding `jest` to a new project. And when I add my script to run my test in the `package.json`, I asked myself:

> How libraries can expose executable script? And how the project can use it?

Every day I use **Command Line Interface**, and I am sure you do too. Just look at my `package.json`:

```json
{
  "scripts": {
    "lint": "eslint .",
    "test": "jest .",
    "build": "webpack ."
  }
}
```

A lot of libraries have **CLI**, `jest`, `vitest`, `webpack`, `rollup`, ...

In this article you will see that thanks to the `package.json` you can expose executable file.
And that, at the installation of library, the package manager creates **symbolic link** in a binary folder inside `node_modules`
to the library CLI.

Let's make a CLI together :)

> **Note:** In the article, I will talk about `npm` and `yarn` on a UNIX machine. If you use another package manager, for example `pnpm` you can also
do everything I do in this article. And the command are here to help you do it. It can also act differently if you use `volta` for managing your version your package manager versions.

----

## Initialization of the library

Let's make a new folder and go inside:

```bash
mkdir example-js-cli
cd example-js-cli
```

Now, we are going to create the `package.json` thanks to `yarn`:

```bash
yarn init
```

We are going to let all the values empty.

> **Note:** You can do the same with `npm` by launching `npm init`. The final `package.json` will differ a little bit but not so much ;)

We end up with the following `package.json`:

```json
{
  "name": "example-js-cli",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}
```

----

## Let's make some code

In this article, we are going to implement a simple CLI, that will print some information about your PC:
- your username
- the uptime of the computer
- the operating system
- the total memory

We are just going to use the `os` module of **nodejs** and the library **chalk** that allow us to make some stylized console logs easily.

> **Note:** We can stylized console logs and do a lot of other things than `console.log` like `console.time`, `console.count`. If you want to know more about it
you can read my article [Unknown console API in JS](https://www.romaintrotard.com/posts/unknown-console-api-in-js).

No more talking, we want some code.

Just add the `chalk` library:

```bash
yarn add chalk
```

> **Warning:** Do not use `devDependencies` in a CLI! Otherwise, it will not work because the dependencies will not be added!

And here is the code, I have written:

```js
const os = require("os");
const chalk = require("chalk");

const colors = [
  "red",
  "yellow",
  "green",
  "cyan",
  "blue",
  "magenta",
];

/**
 * Function to make some rainbow <3
 */
function rainbowText(value) {
  return value
    .split("")
    .map((letter, index) =>
      chalk[colors[index % colors.length]].bold(letter)
    )
    .join("");
}

const SECONDS_BY_HOUR = 3600;
const SECONDS_BY_DAY = SECONDS_BY_HOUR * 24;

/**
 * Seconds doesn't mean anything for me.
 * Let's transform it to day / hour / minute
 */
function secondsToHumanTime(seconds) {
  const days = Math.floor(seconds / SECONDS_BY_DAY);
  const hours = Math.floor(
    (seconds - days * SECONDS_BY_DAY) / 3600
  );
  const minutes = Math.floor(
    (seconds -
      hours * SECONDS_BY_HOUR -
      days * SECONDS_BY_DAY) /
      60
  );

  const array = [
    {
      value: days,
      label: "day",
    },
    {
      value: hours,
      label: "hour",
    },
    {
      value: minutes,
      label: "minute",
    },
  ];

  // Do not insert 0 values
  return (
    array
      .filter(({ value }) => value > 0)
      // Trick to make plural when needed
      .map(
        ({ value, label }) =>
          `${value}${label}${value > 1 ? "s" : ""}`
      )
      .join(" ")
  );
}

/**
 * Mb is way more readable for me
 */
function byteToMegaByte(byteValue) {
  return Math.floor(byteValue / Math.pow(10, 6));
}

console.log(
  `Hello ${rainbowText(os.userInfo().username)}\n`
);
console.log(
  chalk.underline(
    "Here you will some information about your pc"
  )
);

console.table([
  {
    info: "Uptime",
    value: secondsToHumanTime(os.uptime()),
  },
  {
    info: "Operating System",
    value: os.version(),
  },
  {
    info: "Total memory",
    value: `${byteToMegaByte(os.totalmem())}Mb`,
  },
]);
```

It's a lot of code that I give without explanation. But it should be fine to understand.

If you have some questions about it, do not hesitate to PM me on Twitter :)


And now we can run this script by running:

```bash
node index.js
```

This is what I get:

![Own PC information after launching the script](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3l1llqurvzqmfycl7qyv.png)

Otherwise, you can also add a `scripts` entry into our `package.json`:

```json
{
  "scripts": {
    "start": "node index.js"
  }
}
```

And now we can just launch it with:

```bash
yarn start
```

But this is not the goal of this article, we want the **world** to use our wonderful library.

----

## Let's make the CLI stuff

It would be wonderful if any project could just add the following scripts in their `package.json`:

```json
{
  "scripts": {
    "computerInfo": "giveMeComputerInfo"
  }
}
```

With `giveMeComputerInfo` that is our **CLI**. Instead of located our exported scripts in their **node_modules** and run it with **node**.

As you may guess it, you will just edit your `package.json` to do it.

The property to do that is named **bin** (meaning **binary**).

There is two ways to reference our script.
The first one is just to put the path to your script.

```json
{
  "bin": "./index.js"
}
```

In this case the name of the executable is the name of our package, i.e. `example-js-cli`.

Well, not the best. Here is the second method to the rescue:

```json
{
  "bin": {
    "giveMeComputerInfo": "./index.js"
  }
}
```

**And that's all?**

Actually, no. If you add publish the library. And add it in another project, and add the scripts' entry written a bit upper on the page.
It will... just fail.

**Why does it fail?**

Because, we need to tell to the unix shell what is the interpreter that needs to run the script.
We can do this thanks to the following **shebang** syntax:

```bash
#!/usr/bin/env node
```

Put this at the top of the `index.js` file. And here we go.

----

## How does it work?

When you add a library, the package manager will detect the property `bin` and will make symbolic links to each scripts
to the `node_modules/.bin` directory.

When a CLI is called in `scripts`, it will first look at the `node_modules/.bin` and then will look at every directory
that are registered in your `PATH`. For example for UNIX users, in the `PATH` you will find:
- `/usr/.bin`
- `/usr/local/bin`
- ...

You can all the directory, by launching:

```bash
echo $PATH
```

## Add a package globally on your machine

You may not know, but you can also add package **globally** on your operating system.

You can do it with `yarn` with:

```bash
yarn global add theWantedPackage
```

And with `npm`:

```bash
npm install -g theWantedPackage
```

Then, you will be able to execute CLI of the `theWantedPackage` from everywhere on your computer.

----

### How does global package work?

As you can imagine when you add a package globally, the package is installed in a `node_modules` somewhere on your computer and
a symbolic link will be created into `/usr/local/bin/`.

For example, let's add our package globally:

```bash
yarn global add example-js-cli
```

You can now see where is located the executable `giveMeComputerInfo` by launching:

```bash
which giveMeComputerInfo
```

Effectively, it's located in the `/usr/local/bin` folder, but it's only a **symbolic link**.

But where is the real file?

```bash
ls -l /usr/local/bin/giveNeComputerInfo
```

The result depends on if you use `yarn` or `npm`.

| Package manager | localization |
| :-----------: | :-------: |
| yarn |`/usr/local/lib/node_modules/bin` |
| npm |`/usr/local/share/.config/yarn/global/node_modules/bin` |

As you can see, the symbolic link points to the **symbolic link** inside the `node_modules/.bin` folder and not directly to the script.

**Fun fact:** `yarn` create a global project that has a `package.json`. When you add globally a dependency it will be
added inside this `package.json`.

----

## Conclusion

You can expose **CLI** thanks to the **bin** property of your **package.json**. It's even possible to export multiple CLI.

Then, when you install the library in a project, the package manager will detect the property and will make some **symbolic link**
to the **.bin** directory in **node_modules**. This symbolic link point to the real script file.

When you launch a script command, the package manager will look at the `.bin` folder then will look to every directory registered in your
**PATH**.

It's also possible to add a CLI globally on your system, in this case the library is installed in a "global" `node_modules` directory
and a symbolic link is created inside the `/usr/local/bin` folder pointer to the symbolic link inside the `node_modules/.bin` folder.

----

Do not hesitate to comment and if you want to see more, you can follow me on [Twitch](https://twitch.tv/rainbowapps ) or go to my [Website](https://www.romaintrotard.com). And here is a little link if [you want to buy me a coffee](https://ko-fi.com/romaintrotard) :coffee: