How to build a CLI using NodeJS 💻

Mukesh - Jan 4 '22 - - Dev Community

How to build a CLI using NodeJS 💻

CLI (Command Line Interface) is one of the most basic and powerful applications ever created by mankind. We all use CLI every day, whether it be npm, git, or any other CLI. Does your daily basis workflow have something that you have to do over and over again? 🤔. The chances are that it could be automated using CLI ✨

So let's get started 🏄

Today we are going to be building a CLI which would generate starter templates with TailwindCSS, ESLint, and Prettier pre-installed.

Prerequisites

Here are a few tools which you would need to follow along with the tutorial:

  1. A LTS (Long Term Support) version of NodeJS installed.
  2. A text editor.

Setting up the project

Let's initialize a NodeJS project

  1. Open up your terminal
  2. Create a folder for your project
mkdir tailwindcli
Enter fullscreen mode Exit fullscreen mode
  1. Navigate into it
cd tailwindcli
Enter fullscreen mode Exit fullscreen mode
  1. Initializing a NodeJS project
npm init
Enter fullscreen mode Exit fullscreen mode

Building the CLI

Now that we have our NodeJS setup ready. Let's start building our CLI

  1. Create a folder named bin in the root directory of your project folder.
  2. Create a file called index.js in the bin folder. This is going to be the main file of the CLI.
  3. Now open up the package.json file and change the value of the key main to ./bin/index.js.
  4. Now add an entry into the package.json file called bin and add set its key to tcli and its value to ./bin/index.js

The word tcli is the keyword which we would be using to call our CLI.

After making the changes the package.json file should look something like this:

{
  "name": "tailwindcli",
  "version": "1.0.0",
  "description": "A CLI for generating starter files with TailwindCSS pre-installed",
  "main": "./bin/index.js",
  "bin": {
    "tcli": "./bin/index.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": ["cli", "tailwindcss", "nodejs"],
  "author": "Your name",
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode
  1. Open bin/index.js file and add this line at the top of the file
#! /usr/bin/env node
Enter fullscreen mode Exit fullscreen mode

The line starting with a #! is called a shebang line. A shebang line is used to tell the absolute path to the interpreter that will run the below code.

Let's add some JS code so that we can test the CLI out 🚀.

  1. Adding some JS code
console.log('The CLI is working 🚀');
Enter fullscreen mode Exit fullscreen mode
  1. Installing and testing the CLI out

A CLI is meant to be called from anywhere in the system so let's install it globally by using the following command

npm install -g .
Enter fullscreen mode Exit fullscreen mode

Let's test our CLI by running tcli command.

🎉 Tada, our CLI is working

Installing and working with Inquirer

Inquirer is a package that is used to make interactive CLI interfaces. Such as :

To install run the following command

npm install inquirer
Enter fullscreen mode Exit fullscreen mode

Adding the boilerplate of inquirer

Here is the boilerplate for inquirer

#! /usr/bin/env node

const inquirer = require('inquirer');

inquirer
  .prompt([
    /* Pass your questions in here */
  ])
  .then((answers) => {
    // Use user feedback for... whatever!!
  });
Enter fullscreen mode Exit fullscreen mode

Adding questions

We have to pass questions as objects. Let's add the first question asking about the JS framework.

#! /usr/bin/env node

const inquirer = require('inquirer');

inquirer
  .prompt([
    {
      type: 'list',
      name: 'framework',
      message: 'Choose the JS framework which you are using:',
      choices: ['React', 'NextJS', 'Angular', 'Svelte', 'VueJS'],
    },
  ])
  .then((answers) => {});
Enter fullscreen mode Exit fullscreen mode

Let's break it down and understand what each part means

  • type : Inquirer currently has 9 different CLI user interfaces.




  • name : Inquirer returns the answers in the form of an object. For example:

    • If we add console.log(answers);, then we would get a result something like this

So here the name is the key of the object

  • message : It is the question which is been displayed to the user
  • choices : These are the options given to user

Cleaning up the codebase [Optional]

We could create a folder inside the bin folder named utils and create a file inside the utils folder named questions.js. In the questions.js we can store the questions and import them into the index.js file

utils/questions.js

// This question would be shown at the starting
const questions = [
  {
    type: 'list',
    name: 'framework',
    message: 'Choose the JS framework which you are using:',
    choices: ['React', 'NextJS', 'Angular', 'Svelte', 'VueJS'],
  },
];

// This question would be shown only when the user choose either React or NextJS
const questionsTs = [
  {
    type: 'list',
    name: 'typescript',
    message: 'Does your project use TypeScript?',
    choices: ['Yes', 'No'],
  },
];

module.exports.questions = questions;
module.exports.questionsTs = questionsTs;
Enter fullscreen mode Exit fullscreen mode

index.js

#! /usr/bin/env node

const inquirer = require('inquirer');

const { questions, questionsTs } = require('./utils/questions.js');

inquirer.prompt(questions).then((answers) => {
  // Use user feedback for... whatever!!
});
Enter fullscreen mode Exit fullscreen mode

Adding logic

It's time to add some logic as we are doing creating questions.

Accessing answers to questions are similar to accessing the value of a key from an object. The value of the answer of a specific question is answers.<name-of-the-question>

As we are creating starter files, let's use ShellJS to run commands like git clone, mkdir...

Installing ShellJS

To install ShellJS run the following command

npm install shelljs
Enter fullscreen mode Exit fullscreen mode

Working with ShellJS

Let's add a few if and else blocks for logic

#! /usr/bin/env node

const inquirer = require('inquirer');
const shell = require('shelljs');

const { questions, questionsTs } = require('./utils/questions.js');

inquirer.prompt(questions).then((answers) => {
  if (answers.framework === 'React') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        // If the user has choosen React and want to use TypeScript
      } else {
        // If the user has choosen React but doesn't want to use TypeScript
      }
    });
  } else if (answers.framework === 'NextJS') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        // If the user has choosen NextJS and want to use TypeScript
      } else {
        // If the user has choosen NextJS but doesn't want to use TypeScript
      }
    });
  else if (answers.framework === 'Svelte') {
    // If the user has choosen Svelte
  } else {
    // If the user has choosen VueJS
  }
});
Enter fullscreen mode Exit fullscreen mode

Let's find some templates for the JS frameworks integrated with TailwindCSS

Thanks a lot to the wonderful people who have made these great templates for the community ✨

To run a git clone command, use ShellJS we have just used the exec method

shell.exec('git clone <repo-link>');
Enter fullscreen mode Exit fullscreen mode

Let's fill up the if and else blocks now

#! /usr/bin/env node

const inquirer = require('inquirer');
const shell = require('shelljs');

const path = process.cwd();

const { questions, questionsTs } = require('./utils/questions.js');

inquirer.prompt(questions).then((answers) => {
  if (answers.framework === 'React') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        shell.exec(`mkdir ${answers.projectName}`);
        shell.exec(
          `git clone https://github.com/GKaszewski/react-tailwind-typescript-template ${answers.projectName}`
        );
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      } else {
        shell.exec(`mkdir ${answers.projectName}`);
        shell.exec(
          `git clone https://github.com/YashKumarVerma/react-tailwind-template ${answers.projectName}`
        );
        console.log('🛠️  Successfully build the required files');
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      }
    });
  } else if (answers.framework === 'NextJS') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        shell.exec(`mkdir ${answers.projectName}`);
        shell.exec(
          `git clone https://github.com/avneesh0612/next-starter ${answers.projectName}`
        );
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      } else {
        shell.exec(`mkdir ${answers.projectName}`);
        shell.exec(
          `git clone https://github.com/Neeraj1005/Nextjs-tailwind-template ${answers.projectName}`
        );
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      }
    });
  } else if (answers.framework === 'Svelte') {
    shell.exec(`mkdir ${answers.projectName}`);
    shell.exec(
      `git clone https://github.com/jhanca-vm/Svelte-Tailwind ${answers.projectName}`
    );
    shell.cd(`${path}/${answers.projectName}`);
    shell.exec(`npm i`);
    console.log(
      '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
    );
  } else {
    shell.exec(`mkdir ${answers.projectName}`);
    shell.exec(
      `git clone https://github.com/web2033/vite-vue3-tailwind-starter ${answers.projectName}`
    );
    shell.cd(`${path}/${answers.projectName}`);
    shell.exec(`npm i`);
    console.log(
      '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

Cleaning up the codebase [Optional]

Let's create a new file in utils folder named links.js. Let's create a hashmap where we will store the GitHub repository links for the template repos.

let links = new Map([
  ['React', 'https://github.com/YashKumarVerma/react-tailwind-template'],
  [
    'React-TS',
    'https://github.com/GKaszewski/react-tailwind-typescript-template',
  ],
  ['NextJS', 'https://github.com/Neeraj1005/Nextjs-tailwind-template'],
  ['NextJS-TS', 'https://github.com/avneesh0612/next-starter'],
  ['Svelte', 'https://github.com/jhanca-vm/Svelte-Tailwind'],
  ['Vue', 'https://github.com/web2033/vite-vue3-tailwind-starter'],
]);

module.exports = links;
Enter fullscreen mode Exit fullscreen mode

Let's import utils/index.js and replace the GitHub template repositories links.

#! /usr/bin/env node

const inquirer = require('inquirer');
const shell = require('shelljs');

const path = process.cwd();

const { questions, questionsTs } = require('./utils/questions.js');
const links = require('./utils/links.js');

inquirer.prompt(questions).then((answers) => {
  if (answers.framework === 'React') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log('📁 Created a folder for the project');
        shell.exec(`git clone ${links.get('React-TS')} ${answers.projectName}`);
        console.log(`🖨️  Cloned started files into ${answers.projectName}`);
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      } else {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log('📁 Created a folder for the project');
        shell.exec(`git clone ${links.get('React')} ${answers.projectName}`);
        console.log(`🖨️  Cloned started files into ${answers.projectName}`);
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      }
    });
  } else if (answers.framework === 'NextJS') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log('📁 Created a folder for the project');
        shell.exec(
          `git clone ${links.get('NextJS-TS')} ${answers.projectName}`
        );
        console.log(`🖨️  Cloned started files into ${answers.projectName}`);
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      } else {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log('📁 Created a folder for the project');
        shell.exec(`git clone ${links.get('NextJS')} ${answers.projectName}`);
        console.log(`🖨️  Cloned started files into ${answers.projectName}`);
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
        );
      }
    });
  } else if (answers.framework === 'Svelte') {
    shell.exec(`mkdir ${answers.projectName}`);
    console.log('📁 Created a folder for the project');
    shell.exec(`git clone ${links.get('Svelte')} ${answers.projectName}`);
    console.log(`🖨️  Cloned started files into ${answers.projectName}`);
    shell.cd(`${path}/${answers.projectName}`);
    shell.exec(`npm i`);
    console.log(
      '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
    );
  } else {
    shell.exec(`mkdir ${answers.projectName}`);
    console.log('📁 Created a folder for the project');
    shell.exec(`git clone ${links.get('Vue')} ${answers.projectName}`);
    console.log(`🖨️  Cloned started files into ${answers.projectName}`);
    shell.cd(`${path}/${answers.projectName}`);
    shell.exec(`npm i`);
    console.log(
      '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

Beautification using Chalk

We add colors to the text by using Chalk

To install chalk, use the following command:

npm install chalk
Enter fullscreen mode Exit fullscreen mode

Let's now import chalk into our index.js file

const chalk = require('chalk');
Enter fullscreen mode Exit fullscreen mode

Chalk have few pre built color methods

Chalk also offers a hex method by which you can use any color

Let's add green color to our success output

console.log(chalk.green('Hey 👀, I am a green colored text')); // This is how we can add colors by using chalk
Enter fullscreen mode Exit fullscreen mode
#! /usr/bin/env node

const inquirer = require('inquirer');
const shell = require('shelljs');
const chalk = require('chalk');

const path = process.cwd();

const { questions, questionsTs } = require('./utils/questions.js');
const links = require('./utils/links.js');

inquirer.prompt(questions).then((answers) => {
  if (answers.framework === 'React') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log(chalk.green('📁 Created a folder for the project'));
        shell.exec(`git clone ${links.get('React-TS')} ${answers.projectName}`);
        console.log(
          chalk.green(`🖨️  Cloned started files into ${answers.projectName}`)
        );
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          chalk.green(
            '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
          )
        );
      } else {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log(chalk.green('📁 Created a folder for the project'));
        shell.exec(`git clone ${links.get('React')} ${answers.projectName}`);
        console.log(
          chalk.green(`🖨️  Cloned started files into ${answers.projectName}`)
        );
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          chalk.green(
            '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
          )
        );
      }
    });
  } else if (answers.framework === 'NextJS') {
    inquirer.prompt(questionsTs).then((answersTs) => {
      if (answersTs.typescript === 'Yes') {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log(chalk.green('📁 Created a folder for the project'));
        shell.exec(
          `git clone ${links.get('NextJS-TS')} ${answers.projectName}`
        );
        console.log(
          chalk.green(`🖨️  Cloned started files into ${answers.projectName}`)
        );
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          chalk.green(
            '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
          )
        );
      } else {
        shell.exec(`mkdir ${answers.projectName}`);
        console.log(chalk.green('📁 Created a folder for the project'));
        shell.exec(`git clone ${links.get('NextJS')} ${answers.projectName}`);
        console.log(
          chalk.green(`🖨️  Cloned started files into ${answers.projectName}`)
        );
        shell.cd(`${path}/${answers.projectName}`);
        shell.exec(`npm i`);
        console.log(
          chalk.green(
            '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
          )
        );
      }
    });
  } else if (answers.framework === 'Svelte') {
    shell.exec(`mkdir ${answers.projectName}`);
    console.log(chalk.green('📁 Created a folder for the project'));
    shell.exec(`git clone ${links.get('Svelte')} ${answers.projectName}`);
    console.log(
      chalk.green(`🖨️  Cloned started files into ${answers.projectName}`)
    );
    shell.cd(`${path}/${answers.projectName}`);
    shell.exec(`npm i`);
    console.log(
      chalk.green(
        '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
      )
    );
  } else {
    shell.exec(`mkdir ${answers.projectName}`);
    console.log(chalk.green('📁 Created a folder for the project'));
    shell.exec(`git clone ${links.get('Vue')} ${answers.projectName}`);
    console.log(
      chalk.green(`🖨️  Cloned started files into ${answers.projectName}`)
    );
    shell.cd(`${path}/${answers.projectName}`);
    shell.exec(`npm i`);
    console.log(
      chalk.green(
        '👨‍💻  Successfully installed all the required dependencies\nHappy hacking 🚀'
      )
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

Publishing it to npm 🚀

We have successfully completed building our CLI 🥳. Let's now deploy it to npm, so that other developers can use our CLI.

Creating a npm account

Go over to npmjs.org and create an account and make sure that you are verifying it as well

Unique package name

npm packages have unique names. npm doesn't allow publishing a package with a name that is already taken. Go over to npmjs.org and check whether your package name is taken or not.

tailwindcli is already taken by this package. So I have to change the name to tailwindcsscli

Changing name of the package

If your package is unique and is not taken, skip this step, if it isn't then follow along with this step.

  1. Open package.json file
  2. Change the value of the key name to a unique name, in my case I am changing it to tailwindcsscli

Adding keywords

Let's add a few keywords related to our package. As we have built a CLI during this tutorial, let's have the following as keywords :

  • cli
  • tailwindcss
  • nodejs

Adding license

Check out license-templates GitHub repository for license templates which you could use in your project. In my case, I am using MIT license

Adding repository link

If you have a repository on any git provider, such as GitHub, GitLab, you could link to that in a new entry named repository with the keys as type and url and the values as git and git+<your-git-repo-link>.git respectively. It would look something like this

"repository": {
  "type": "git",
  "url": "git+<your-git-repo-link>.git"
}
Enter fullscreen mode Exit fullscreen mode

In my case, the repo link is https://github.com/Kira272921/tailwindcsscli. So it would look something like this

"repository": {
  "type": "git",
  "url": "git+https://github.com/Kira272921/tailwindcsscli.git"
}
Enter fullscreen mode Exit fullscreen mode

Adding link to bugs reports

Let's add the link to the site/place where the users do report bugs about our package. Generally, it would be the link to the issues page in the GitHub repository

"bugs": {
  "url": "https://github.com/Kira272921/tailwindcsscli/issues"
}
Enter fullscreen mode Exit fullscreen mode

Adding link to the homepage

Let's add the link to the homepage of our npm package. Generally, it would be the link to the README link of the GitHub repository

"homepage": "https://github.com/Kira272921/tailwindcsscli/issues#readme"
Enter fullscreen mode Exit fullscreen mode

Login into your npm account via npm CLI

Let's now login in to our npm account via npm CLI so that we can publish our package to npm. To login into your npm account, run the following command and enter the correct credentials.

npm login
Enter fullscreen mode Exit fullscreen mode

Publishing your npm package

Let's now publish our npm package by using the following command

npm publish
Enter fullscreen mode Exit fullscreen mode

😱 Oh no! I got an error

Let's change the name of our package accordingly and publish using the command recommended. My package.json looks something like this now

{
  "name": "@kira272921/tailwindcsscli",
  "version": "1.0.0",
  "description": "A CLI for generating starter files for different JS frameworks with tailwindCSS pre-installed",
  "main": "./bin/index.js",
  "bin": {
    "tcli": "./bin/index.js"
  },
  "scripts": {
    "start": "node ./bin/index.js"
  },
  "keywords": ["cli", "tailwindcss", "nodejs"],
  "author": "Kira272921",
  "license": "MIT",
  "dependencies": {
    "inquirer": "^8.2.0",
    "shelljs": "^0.8.4"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/Kira272921/tailwindcsscli.git"
  },
  "bugs": {
    "url": "https://github.com/Kira272921/tailwindcsscli/issues"
  },
  "homepage": "https://github.com/Kira272921/tailwindcsscli/issues#readme"
}
Enter fullscreen mode Exit fullscreen mode

Let's try publishing it now again by using the following command

npm publish --access=public
Enter fullscreen mode Exit fullscreen mode

Fingers crossed 🤞. Yay! We have successfully published our CLI to npm 🥳

The end

The code for this tutorial is available on Github
https://github.com/Kira272921/tailwindcsscli

That's for this blog folks. Hope that you have learned something new from this blog post. Meet y'all in the next blog post 👋.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .