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

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...

How to solve Puppeteer TimeoutError: Navigation timeout of 30000 ms exceeded

During the automation of multiple tasks on my job and personal projects, i decided to move on  Puppeteer  instead of the old school PhantomJS. One of the most usual problems with pages that contain a lot of content, because of the ads, images etc. is the load time, an exception is thrown (specifically the TimeoutError) after a page takes more than 30000ms (30 seconds) to load totally. To solve this problem, you will have 2 options, either to increase this timeout in the configuration or remove it at all. Personally, i prefer to remove the limit as i know that the pages that i work with will end up loading someday. In this article, i'll explain you briefly 2 ways to bypass this limitation. A. Globally on the tab The option that i prefer, as i browse multiple pages in the same tab, is to remove the timeout limit on the tab that i use to browse. For example, to remove the limit you should add: await page . setDefaultNavigationTimeout ( 0 ) ;  COPY SNIPPET The setDefaultNav...