Skip to main content

Creating CLI Executable global npm module

JavaScript has quickly became the most popular language on the planet and it is growing like anything. After ES6 and ES7, JavaScript official joined club of Object Oriented Programming languages. With async/await, it has become very easy to write asynchronous programs. The bigger success of JavaScript came with launch of Node.js which pushed JavaScript on server stack. Hence JavaScript is now found everywhere, from mobile applicationsdesktop applicationsIoT devicesServers to every day web browsers.
But above all, what I like the most about JavaScript, Node.js and npm together is the ability to build CLI applications. Somewhere in your life you might have opened the terminal and executed some command like git clone xxx or rm -rf xxx, these are all CLI commands referring to some program like git and rm. We can also install a npm module globally which means we can refer to that module from anywhere in our system using terminal commands.
When you install a module globally, npm will place that module inside a fixed folder (let’s call it npm folder), for example in Windows it could be$user/AppData/Roaming/npm or any other folder depending on your system. While installing a global module, npm processes package.json of that module and looks for bin field. bin field is an object with key being the terminal command and value being the .js file in the module which needs to be executed when user executes that command. If bin field is non-empty, then npm creates necessary files inside npm folder so that user can use the commands specified in package.json. These file generally do not have any extension and their file name is the same as command name that they will execute on. In Windows, .cmd extension file is also generated along with previous file to make sure execution of .js file with Node.js only. I will explain this bit later.
So let’s create our first global module. First thing to remember is, a global npm module is just like local npm module with few extra information in package.json file. We are going to develop a CLI application where user can execute greet command and a random greeting prints to the terminal in any random language.
Ok, so first we need to create a sample package.json file to work with. Let’s create a folder greeting-project and execute npm init -y command inside it. This will create a dummy package.jsonfile to work with. Our folder structure should look like below.
greeting-project
|_ bin\
|_ lib\
|_ package.json
bin folder will contain all executable .js files, in our case there should be index.js (name of the file can be anything) file inside it because when user will execute greet command, that’s the file we want to execute. libfolder contains other files which index.js might use. We can also install any dependency modules which our CLI application may depend on. In our application, we will use lodash and colors modules to help our module with some things which you will see next. These modules must be installed locally, hence we will execute command npm install --save lodash colors and npm will install these modules for us.
Finally, our package.json will look like below.
{
    "name": "greeting-project",
    "version": "1.0.0",
    "dependencies": {
        "colors": "^1.2.1",
        "lodash": "^4.17.5"
    }
}
There can be many other fields as well but I removed unnecessary fields to make it look clean. Now, we have to add other fields manually to make this module global. But before that, let’s write our program and test it locally. We are going to create few files like below.
greeting-project
|_ bin
  |_ index.js
|_ lib
  |_ greet.js
greet.js file contains all logic of our CLI application module. Content of this file looks like below.
(greet.js)
From above code, you can see that we have imported lodash module on top. Then we created a JavaScript object with key being the language name of greeting and value being the greeting itself in that language. After that, we exported two functions, greet and greetRandomgreet function returns the greeting in the language received by the function and greetRandom returns any random greeting.
index.js file is supposed to execute when user executes greet command in terminal. Initially, what we are going to do is, import greet.js and test the greetRandom function. It’s content look like below.
(index.js)
If you notices the first line of code which is #!/usr/bin/env node which looks obviously suspicious is a shebang which tells operating system what interpreter or application to pass that file to for execution. In our case, it’s node which is obvious from above shebang. But Windows unfortunately do not support shebang, instead it passes a file to the default interpreter or application associated with the extension of that file. Hence npm creates .cmd file inside global npm folder so that Windows will use node interpreter to execute .js files even default application associated with .js extension might be something else.
In index.js, we have imported colorsmodule which will help us print colorful text to the terminal. We also imported greet.js file from lib folder which contains application logic. For test purpose, we will print random greeting to the terminal. This can be done by using greetRandom function from greet.js file. Rest of the code should be obvious to you.
Now let’s test it locally before installing it globally to use it from CLI. To test this program, we will run index.js using node like node ./bin/index.js which will print this in rainbow color to the terminal.
Dobre Utra
Above response can be different in your case as we are printing a random greeting. Our application seems to be working. Now what we want is when we execute greet command in terminal instead of node ./bin/index.jscommand, same response should be shown. That means we need to map greet command with ./bin/index.jsfile. This is done by modifying bin field in package.json as we talked about earlier. The important thing to remember is when we will executegreet command, that command will translate to node ./bin/index.js from the global node folder.
There is one more boolean value field preferGlobal in package.json which if set to true prints warning to the console when user is installing this module locally. This doesn’t prevent module to be installed locally, but this will certainly shed some light on confusion in case user notices. If a module can be used both locally and globally, then preferGlobal is set to false or rather does not added to the package.json as it’s default value is false. In our case, we case we want user to use it both locally and globally, we will not add it in package.json to begin with. Hence, our final package.json will look like below.
{
    "name": "greeting-project",
    "version": "1.0.0",
    "main": "./lib/greet.js",
    "bin": {
        "greet": "./bin/index.js"
    },
    "dependencies": {
        "colors": "^1.2.1",
        "lodash": "^4.17.5"
    }
}
Look carefully at bin field. As we discussed that key of this field is command, in our case it is greet and value is file to execute with that command which is ./bin/index.js in our case. There is one more field main in above package.json which tells node that when somebody is trying to import this module locally like const greeter = require('greeting-project'), then provide him/her ./lib/greet.js file to implement business logic of our application. If main is missing, then node by default will try to pull index.js file from module’s root directory which is clearly missing in our case. Adding mainmakes our module both locally and globally usable. We are not going to test local installation, though.
If you just have one command in your program and that command is same as npm package name of your project then you can use path to bin file directly as value of bin field like {“bin”: “./bin/index.js”} which will be equivalent to {“bin”:{“greeting-project”: “./bin/index.js”}}. But avoid this, so that even package name changes, command won’t change.
Now let’s install our module globally. Generally we have installed global modules like npm install -g package_name where package_name is published module available on npm’s registry. Since we haven’t published our module and we don’t have any intention of publishing it until we done all our testing, we kinda have to trick npm to install from local source code. This can be done using npm install -g local_dir_path where local_dir_path is directory path of source code of the module we want npm to install globally. Since we are inside the folder of our module’s source code, we can use npm install -g ./ which will instruct npm to create symbolic link from global npm folder to the current folder. This will also create greet and greet.cmd files inside global npm folder as well.
Now are free to execute greetcommand from terminal. When I execute greet command in my terminal, I get random greetings. I hope it is working for you too.
Any changes made inside index.jswill reflect immediately because npm created only symbolic link, hence greet command refers to the index.jsfile inside our local source code.
Now let’s understand about command line arguments. When we execute any .js file using Node.js, node provides process variable which contains information about the executing process. process.argv returns the arguments used while executing a .jsfile with node. Let’s add following line to at the end of our index.js and execute greet --lang ru command.
console.log(process.argv);
Execution of greet --lang ru command will print below response to the terminal.
Bonjour
[ 'C:\\Program Files\\nodejs\\node.exe',
  'C:\\Users\\Uday\\AppData\\Roaming\\npm\\node_modules\\greeting-project\\bin\\index.js',
  '--lang',
  'ru' ]
Focus on part inside square brackets. These are the arguments of the process. First element is the path of node interpreter and second element is the path of file being executed. Later elements are space separated text values added after greet or node ./bin/index.js command. We can use this information to execute either greetor greetRandom function depending on user choice.
Let’s modify index.js file to incorporate that logic.
(index.js)
Now, when we execute greet --lang rucommand, only Dobre Utra is getting printed. But when we use greet --langor greet then a random greeting is printed, because lang variable in index.js will be empty in those cases.
Looks like our app is working well. Now it’s time to publish it on npm registry so that other people can use it. This is just like publishing local module with command npm publish. When other people will install our module with command npm install -g greeting-project, npm copies source code from it’s registry to global npm foldercreates necessary files for CLI execution and installs dependencies of our module. Once npm done installing our module, users can execute greet command on their system with ease.

What happens when a user installs module locally?

Well, nothing bad happens. But then user has to define a command that was supposed to be used from terminal, inside package.jsonpackage.jsonhasscripts field which is JSON key-value object where key is short-name of command that is defined as value. For example,
// packag.json{
  "scripts": {
      "greet-ru": "greet --lang ru"
  }
}
Now, user can run this command from terminal using npm like
npm run greet-ru
Also, we have exposed business logic of our app through package.json in mainfield like below.
"main": "./lib/greet.js"
By importing greeting-projectpackage, user can use this business logic however he/she wants in application.

I hope this tutorial was fun for you guys. You can find repo of above example on GitHub at .

I have written more advanced tutorial on making CLI apps more powerful and interactive using command.js and inquirer.js which is available on Medium.

Comments

Popular posts from this blog

4 Ways to Communicate Across Browser Tabs in Realtime

1. Local Storage Events You might have already used LocalStorage, which is accessible across Tabs within the same application origin. But do you know that it also supports events? You can use this feature to communicate across Browser Tabs, where other Tabs will receive the event once the storage is updated. For example, let’s say in one Tab, we execute the following JavaScript code. window.localStorage.setItem("loggedIn", "true"); The other Tabs which listen to the event will receive it, as shown below. window.addEventListener('storage', (event) => { if (event.storageArea != localStorage) return; if (event.key === 'loggedIn') { // Do something with event.newValue } }); 2. Broadcast Channel API The Broadcast Channel API allows communication between Tabs, Windows, Frames, Iframes, and  Web Workers . One Tab can create and post to a channel as follows. const channel = new BroadcastChannel('app-data'); channel.postMessage(data); And oth...

Certbot SSL configuration in ubuntu

  Introduction Let’s Encrypt is a Certificate Authority (CA) that provides an easy way to obtain and install free  TLS/SSL certificates , thereby enabling encrypted HTTPS on web servers. It simplifies the process by providing a software client, Certbot, that attempts to automate most (if not all) of the required steps. Currently, the entire process of obtaining and installing a certificate is fully automated on both Apache and Nginx. In this tutorial, you will use Certbot to obtain a free SSL certificate for Apache on Ubuntu 18.04 and set up your certificate to renew automatically. This tutorial will use a separate Apache virtual host file instead of the default configuration file.  We recommend  creating new Apache virtual host files for each domain because it helps to avoid common mistakes and maintains the default files as a fallback configuration. Prerequisites To follow this tutorial, you will need: One Ubuntu 18.04 server set up by following this  initial ...

Working with Node.js streams

  Introduction Streams are one of the major features that most Node.js applications rely on, especially when handling HTTP requests, reading/writing files, and making socket communications. Streams are very predictable since we can always expect data, error, and end events when using streams. This article will teach Node developers how to use streams to efficiently handle large amounts of data. This is a typical real-world challenge faced by Node developers when they have to deal with a large data source, and it may not be feasible to process this data all at once. This article will cover the following topics: Types of streams When to adopt Node.js streams Batching Composing streams in Node.js Transforming data with transform streams Piping streams Error handling Node.js streams Types of streams The following are four main types of streams in Node.js: Readable streams: The readable stream is responsible for reading data from a source file Writable streams: The writable stream is re...