Go to main content

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:

{
  "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 :)


Let's make a new folder and go inside:

mkdir example-js-cli
cd example-js-cli

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

yarn init

We are going to let all the values empty.

We end up with the following package.json:

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

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.

No more talking, we want some code.

Just add the chalk library:

yarn add chalk

And here is the code, I have written:

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:

node index.js

This is what I get:

Own PC information after launching the script

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

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

And now we can just launch it with:

yarn start

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


It would be wonderful if any project could just add the following scripts in their package.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.

{
  "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:

{
  "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:

#!/usr/bin/env node

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


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:

echo $PATH

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

You can do it with yarn with:

yarn global add theWantedPackage

And with npm:

npm install -g theWantedPackage

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


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:

yarn global add example-js-cli

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

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?

ls -l /usr/local/bin/giveNeComputerInfo

The result depends on if you use yarn or npm.

Package managerlocalization
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.


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.


You can find me on Twitter if you want to comment this post or just contact me. Feel free to buy me a coffee if you like the content and encourage me.