A command-line interface (CLI) is a computer program that processes commands in the form of text inputs and in turn executes system functions. In the early days of computing, the only way to interact with a computer was through the terminal. But today, many users interact with the computer via a graphical user interface (GUI). As a software developer, you are likely to have used a CLI. If you're a Node.js or React developer, you use the npm CLI (almost) every day.
You probably have used a CLI to install or configure software or automate repetitive tasks. There are many use cases for building a CLI and knowing how to do that with your JavaScript and React.js skills can be productive.
Understanding the Turorial's Project
This tutorial will show you how to build a CLI using Node.js and React. The CLI will be used to create a Node.js microservice project. It will be similar to how the create-react-app works in that you can run it using npx or just install it as a global package.
Prerequisite
To follow along with this tutorial, you should know how to use React and Node.js. You also need to have Node.js and npm installed.
Set Up the CLI Project
You're going to build the CLI using Ink, a React component-based library for building interactive CLIs. It uses Yoga to build Flexbox layouts in the terminal, so most CSS-like props are available in Ink as well. Ink is simply a React renderer for the terminal, so all the React features are supported. No need to learn a new syntax specific to Ink.
Run the commands below to create the project.
mkdir create-micro-service && cd create-micro-service
npx create-ink-app
After the command is processed, you should have the project files and dependencies installed, and a symlink created for the CLI. Open your terminal and run the command create-micro-service
. You should get the message "Hello, Stranger" printed out. If you run create-micro-service --help
, you should get the following help message.
Usage
$ create-micro-service
Options
--name Your name
Examples
$ create-micro-service --name=Jane
Hello, Jane
Processing Arguments and Flags
There are two important files in the project directory, namely cli.js and ui.js. The cli.js file is the entry point into the application, and ui.js is the React component that renders the message Hello, Stranger. In cli.js you will find the following code:
#!/usr/bin/env node
"use strict";
const React = require("react");
const importJsx = require("import-jsx");
const { render } = require("ink");
const meow = require("meow");
const ui = importJsx("./ui");
const cli = meow(`
Usage
$ create-micro-service
Options
--name Your name
Examples
$ create-micro-service --name=Jane
Hello, Jane
`);
render(React.createElement(ui, cli.flags));
You will notice that it is using meow to read the command and flags. Afterward, it passes the flags to the ui component and renders it. meow is a helper library to parse arguments. The text passed to meow()
is the help text shown when you call create-micro-service --help
.
Modify the Help Text
The CLI will accept a --name
flag to use as the directory name for the project to scaffold. Let's modify the help text to indicate that. Open cli.js and update the statements from lines 10 to 20 with the code below.
const cli = meow(`
Usage
$ create-micro-service
Options
--name Your project's name.
Examples
$ create-micro-service --name=order-service
`);
That's all that is needed for now. The next step is for it to create the directory with the necessary files and dependencies.
Copy the Project Files and Install the Dependencies
When the CLI executes, it should create a new directory and clone a Node.js project template to it. Afterward, it will install some dependencies needed in the project. Let's start with cloning the project files. There are different ways you can do this, for example, starting a child process and executing a git command to clone the project. For this tutorial, you're going to use degit. degit will make a local copy of a git repository without copying the git history.
Install degit by running the command npm i degit
.
Create a new file named init.js and paste the code below into it.
const degit = require("degit");
exports.clone = async (name) => {
const emitter = degit("github:kazi-faas/function-template-js");
await emitter.clone(`./${name}`);
};
The code above initializes the degit module with the name of the git repository to clone. It is function-template-js repository under the kazi-faas organization. The string value is prefixed with github:
so that it knows the Git server to use. If you use GitLab, it will be gitlab:
.
The clone() function will then clone the files into the specified directory. The directory name will be the --name
flag from the command line. You can find the template’s GitHub repository at github.com/kazi-faas/function-template-js.
After the template is cloned, a package.json file will be added. You can simply use the fs module to create and write to this file. For this tutorial, you're going to use the write-pkg npm package for this. Its single purpose is creating package.json files.
Open your terminal and run the command npm i write-pkg@4
to install the package.
Then add the following require statements in init.js.
const { join } = require("path");
const writePackage = require("write-pkg");
Copy and paste the addPackageJson()
function below to the same file.
exports.addPackageJson = async (name) => {
const pkg = {
name,
description: "A function which responds to HTTP requests",
main: "index.js",
scripts: {
start: "micro",
dev: "micro-dev",
},
};
await writePackage(join(process.cwd(), name, "package.json"), pkg);
};
The addPackageJson()
will create a package.json file for the project. It will use the name
parameter as the package name, and it includes two scripts, namely start and dev.
After adding the package.json file, the CLI should install the dependencies required to run the app. The dependencies it will install are:
- micro: A library for asynchronous HTTP microservices.
- micro-dev: It's the same as micro but specifically for development purposes.
You're going to use pkg-install to install npm packages. pkg-install makes it easy to install npm packages irrespective of the package manager. Open the terminal and run npm i pkg-install
to install it.
After it's installed, open init.js and import the library.
const { install } = require("pkg-install");
Then copy and paste the function below to init.js as well.
exports.installDependencies = async (name) => {
const dir = join(process.cwd(), name);
await install({ micro: "^9.3.4" }, { cwd: dir });
await install({ "micro-dev": "^3.0.0" }, { cwd: dir, dev: true });
};
The install
function is called with the package name and version, and the directory to install. You used two different calls to install()
because we want to install micro-dev
as a devDependency (see the dev: true
option).
Update the React Component
You're going to update ui.js to scaffold a new project and show the progress of that action in the terminal. You will use ink-task-list, a task list component for Ink, to display a list of tasks and their progress. You need to install this component to use it.
Open your terminal and run the command npm i ink-task-list
to install it. After that, open ui.js and the following require statements
const { Task, TaskList } = require("ink-task-list");
const { addPackageJson, clone, installDependencies } = require("./init");
Change the default value for the name
prop from Stranger
to micro-service
. The CLI should use micro-service as the directory/project name if none is supplied by the user.
Update the App
functional component with the code below.
const App = ({ name = "micro-service" }) => {
const [tasks, setTasks] = React.useState([
{ state: "loading", label: "Downloading files." },
]);
React.useEffect(() => {
async function scaffold() {
await clone(name);
setTasks([
{ state: "success", label: "Downloaded files." },
{ state: "loading", label: "Adding package.json." },
]);
await addPackageJson(name);
setTasks((prevState) => [
{ ...prevState[0] },
{ state: "success", label: "Added package.json." },
{ state: "loading", label: "Installing dependencies." },
]);
await installDependencies(name);
setTasks((prevState) => [
{ ...prevState[0] },
{ ...prevState[1] },
{ state: "success", label: "Installed dependencies." },
]);
}
scaffold();
}, []);
return (
<TaskList>
{tasks.map(({ label, state }, index) => (
<Task key={index} label={label} state={state} />
))}
</TaskList>
);
};
The component calls the clone()
, addPackageJson()
and addPackageJson()
functions respectively, and updates the state after each function call. The tasks
variable is an array of objects with state
and label
properties. These properties are used to render the label and an icon that indicates if a task is in progress or completed.
Your CLI in Action
You now have all the code necessary for this tutorial's project. Let's try it out and see that it works. Open your terminal and run the command create-micro-service --name=demo
. This will scaffold a microservice project which you will find in the demo directory.
What Next?
Now that you have built a CLI to scaffold microservice projects, feel free to go wild with your ideas and build CLIs to automate your workflow. You can start with extending the project you built while following this tutorial. You can extend it to include a flag to choose if the dependencies should be installed using npm or yarn. The pkg-install module has an option to set the preferred package manager.
There are other command-line libraries to help you out. You can find some of them listed in the awesome-nodejs GitHub repository. I will also recommend that you read the guidelines at clig.dev. It's an open-source guide to help you write better command-line programs.
You can get this tutorial's code on GitHub—github.com/pmbanugo/create-micro-service-tutorial
Feel free to reach out to me if you have questions. I'm happy to help or point you in the right direction.
Originally published on Telerik blogs: https://www.telerik.com/blogs/how-to-build-cli-node-react