Let’s create a Node CLI for generating files from templates!
Tags: node, npm, javascript, cli
Published: 2023-04-21 11:38:06

**Project:** CFFT - Create Files From Template CLI
- [NPM](https://www.npmjs.com/package/@beezydev/create-files-from-template-cli) 
- [GitHub](https://github.com/dus4nstojanovic/create-files-from-template-cli#readme)

In this article, I explain why I created the CLI for generating files based on templates, what issues I had to solve, and what I’ve learned.

You’ll find much useful information about building your CLI:
-	How to setup the app,
-	How to build a colorful logger,
-	How to read command line arguments,
-	How to ask questions using CLI,
-	How to create a configuration file,
-	How to locate a configuration file based on the current terminal location,
-	How to retrieve the package version,
-	How to create tables for CLI,
-	How to publish your package to npm.

## About CFFT

So, what is CFFT CLI?

It is a simple, but powerful Node CLI that can be used to generate a list of files from templates. It is a file generator. We can use it regardless of a framework – React, Angular, Vue, Express, .NET, Java, Python, … The only requirement is to have Node.js installed on your machine.

**Features:**
•	Create a **custom file structure** using a terminal,
•	**Search and replace** - Replace file content with a custom text or file name,
•	Create **multiple templates**,
•	Set options directly as a **CLI argument**, or **answering on CLI questions**, or:
•	Set **defaults** by configuring a CLI using a **.config JSON** file for each template - *cfft.config.json*.

See [npm](https://www.npmjs.com/package/@beezydev/create-files-from-template-cli) package for more info.

##Why I built it?
I am a full stack developer, focused on a front-end, currently working on a React-based project. I also work with, and/or understand other frameworks and languages, such as Node.js, Nest.js, Next.js, Angular, .NET, etc.  Angular, for example, has a great CLI that helps you to create components and other files using an interactive command line interface. It saves you time by a lot. I decided to build something like that to use on my React projects – but not to limit only to that – I wanted to create something independent of a framework. I built it to save development time spent on repetitive tasks since I cached myself copying/pasting and renaming the same file structure repeatedly.

##Let’s build the app!

>Note: This is not the whole app. I am explaining only some interesting parts of it. Check [GitHub](https://github.com/dus4nstojanovic/create-files-from-template-cli) to see the code.

###Setup the app
I decided to use the following languages and packages:
-	[node.js](https://nodejs.org/en) – this is a node CLI application, so we need a node installed on our machine,
-	[typescript](https://www.typescriptlang.org/) – for type safety,
-	[esbuild](https://esbuild.github.io/) – is an easy-to-configure bundler,
-	[jest](https://jestjs.io/) and [ts-jest](https://www.npmjs.com/package/ts-jest) – for testing,
-	[arg](https://www.npmjs.com/package/arg) – for reading command line arguments,
-	[chalk](https://www.npmjs.com/package/chalk) – for colorful console logs,
-	[cli-table](https://www.npmjs.com/package/cli-table) – for writing tables in a console,
-	[inquirer](https://www.npmjs.com/package/inquirer) – for asking questions.

To start developing, create a new project, initialize a new node project by creating a *package.json* (`npm init` command), and install the packages you need for your CLI. If you wish to use GIT, initialize it using the `git init` command.

The next step is to create the start (executable) file and bundle the app.

Create a new file in your source directory:

*/src/index.ts*
```javascript
const run = () => {
  console.log('Hello World');
};

run();
```

Create **esbuild.js** in a root folder to configure bundling:

*esbuild.js*
```javascript
require("esbuild")
  .build({
    entryPoints: ["src/index.ts"],
    bundle: true,
    outdir: "./dist",
    platform: "node",
    loader: { ".ts": "ts" },
    minify: process.env.NODE_ENV !== "development",
    external: ["fs", "path", "util"],
  })
  .catch(() => process.exit(1));
```
- **entryPoints** – path to the entry file (where to start),
- **bundle** – should bundle,
- **outDir** – where to store the result (the output),
- **loader** – we tell the bundler that we want to use typescript,
- **minify** – optimizes our code by making it smaller
- **external** – what to exclude from the bundle (we don’t need *fs*, *path*, and *util* since these are already included in node).

There are many other bundles in the market, such as *webpack*, *swc*, *turbopack* and *parcel*, but I decided to use *esbuild* since it is easy to configure to work with typescript.

###Logger

Logs are very important for any command line application. It is the application's way to communicate with users. 

I tend to create colorful messages filled with emojis, to make the console more interesting. For example:

![using chalk - example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/afgflbfpmjcl0sv83zfu.png)

To colorize my logs, I used a [chalk](https://www.npmjs.com/package/chalk) package and created a logger utility:

*/src/logger.ts*
```javascript
const log = (...message: any[]) => write(...message);

const success = (...message: any[]) => write(chalk.green(...message));

const warning = (...message: any[]) => write(chalk.yellow(...message));

const info = (...message: any[]) => write(chalk.blue(...message));

const error = (...message: any[]) => write(chalk.red(...message));

const debug = (...message: any[]) =>
  isDebug() ? write(chalk.whiteBright("🔧 [DEBUG]", ...message)) : undefined;

const write = (...message: any[]): void => {
  const shouldWrite = process.env.APP_ENV !== "test";

  if (shouldWrite) {
    console.log(...message);
  }
};

export default { success, warning, info, error, log, debug };
```

###Read command line arguments

Reading CLI arguments is also the essential part of every CLI. It is a way to provide additional options to the application. For example:

```sh
cfft --template css --fileName my-styles
```

Here we tell our CLI to use the *css* template and to create the *my-styles* file structure.

It is possible to extract command line arguments without installing any additional package:

```javascript
var arguments = process.argv;
  
console.log(arguments);
```

…but I decided to install the [arg](https://www.npmjs.com/package/arg) package. It is a nice utility that stores command line arguments in an object, allows you to specify aliases, does validation for you, and more.

To indicate allowed options, types, and aliases, you need to create an object and pass it to the arg function:

```javascript
import arg from "arg";

export const CLI_ARGS_TYPE = {
  "--fileName": String,
  "--dirPath": String,
  "--template": String,
  "--templatePath": String,
  "--shouldReplaceFileName": String,
  "--fileNameTextToBeReplaced": String,
  "--shouldReplaceFileContent": String,
  "--textToBeReplaced": String,
  "--replaceTextWith": String,
  "--searchAndReplaceSeparator": String,
  "--debug": String,
  "--version": Boolean,
  "--help": Boolean,

  // Aliases
  "-n": "--fileName",
  "-t": "--template",
  "-v": "--version",
};

console.log(arg(CLI_ARGS_TYPE));
```

If you run the following command:

```sh
cfft -n MyFile --template MyTemplate
```

…the result will be:
```sh
{ _: [], '--fileName': 'MyFile', '--template': 'MyTemplate' }
```

> Note: See the [/src/options](https://github.com/dus4nstojanovic/create-files-from-template-cli/tree/master/src/options) folder on GitHub for more info.

###Fill in the missing options by asking questions

Our CLI should be user-friendly. If our user forgets to specify some options, we should ask him to do it.

We can easily achieve that with the [inquirer] (https://www.npmjs.com/package/inquirer) package. This package allows you to ask different types of questions, such as input questions (open-text questions), confirmation questions (Yes/No), etc.

For example, asking a simple input question can be written like this:

```javascript
import inquirer from "inquirer";

(async () => {
  const answer = await inquirer.prompt([
    {
      type: "input",
      name: "fileName",
      message: "Enter file name:",
    },
  ]);

  console.log(answer);
})();
```

Result:
![inquirer example result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6yrim6igic5s0tsw3241.png)

For some questions, you can set a default value, too:

```javascript
import inquirer from "inquirer";

(async () => {
  const answer = await inquirer.prompt([
    {
      type: "input",
      name: "fileName",
      message: "Enter file name:",
      default: "MyFile",
    },
  ]);

  console.log(answer);
})();
```
Result:
![inquirer with default value example result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dd28ejy3q8rwplc2ai5x.png)

>Note: For more information, see [/src/questions.ts](https://github.com/dus4nstojanovic/create-files-from-template-cli/blob/master/src/questions.ts) and [/src/options](https://github.com/dus4nstojanovic/create-files-from-template-cli/tree/master/src/options) on GitHub.

###Create a configuration file

Having a configuration file where the user can set some default values is very useful for CLI applications. You could see previously, that at the time of writing this article, the CFFT CLI had more than 10 different options. It could happen that our users forget to fill them, or they can simply want to use the same value for their template every time. To allow them to specify those values we need a configuration file. 

I faced two issues while implementing this feature:

1.	How to know where is the configuration located – where is the root of the project?
2.	How to know does it exist or not?

When I have a problem as described above, I usually split it into multiple steps:
1.	How to know the current terminal path?
2.	How to know if the configuration file is in a parent directory, or in a grandparent directory, etc?
3.	How to know when to stop searching?

CFFT, logically, creates files from a template on a current terminal path. To know that path, you can use the following Node.js function:

```sh
process.cwd()
```

To check does our file exist on the current path, we should try to read it. If it exists, that is a success, if not, the function will throw an exception, so in that case, we check in the parent folder.

To search parent directories, clearly, we need a recursion.

And lastly, if the current path is the same as the previous path, that means that we’ve reached to last parent directory and that our file doesn’t exist.

This is the function that combines everything that is explained:

```javascript
export const findConfig = async (
  pathArg = ".",
  previousPath?: string
): Promise<Config> => {
  const searchPath = path.join(process.cwd(), pathArg, CONFIG_FILE_NAME);
  const folderPath = resolve(path.join(process.cwd(), pathArg));
  const currentPath = resolve(searchPath);

  if (currentPath === previousPath) return null as any;

  Logger.debug(`Searching for config. Path: ${currentPath}`);

  try {
    const file = await promisify(fs.readFile)(searchPath);

    Logger.debug(`Config file found: ${currentPath}`);

    const config = JSON.parse(file.toString()) as Config;
    config.folder = folderPath;
    config.path = currentPath;

    return config;
  } catch (error) {
    return await findConfig(path.join(pathArg, ".."), currentPath);
  }
};
```

> Note: For more information about the configuration, see the [/src/config](https://github.com/dus4nstojanovic/create-files-from-template-cli/tree/master/src/config) folder on GitHub.

###Retrieve the package version

It can be very useful to show the current package version to our users. The goal is to show it when the `--version` or `-v` argument is provided. This is easily implementable:

```javascript
require("./package.json").version
```

###The --help command

Another very useful and user-friendly command for CLI is the `--help` command. This command should help our users to use our CLI. This is the perfect place for tables. I installed the [cli-table](https://www.npmjs.com/package/cli-table) package.

I decided to show three columns: Command, Alias, and Description.

```javascript
const table = new Table({
    head: ["Command", "Alias", "Description"],
    style: {
      head: new Array(3).fill("cyan"),
    },
  });

  let rows = [
    ["--fileName", "-n", "File name to be used"],
    ["--dirPath", "", "Path to the location where to generate files"],
    ["--template", "-t", "Name of the template to use"],
    ["--templatePath", "", "Path to the specific template folder"],
    ["—-shouldReplaceFileName", "", "Should or not CLI replace a file name",],
    ["--fileNameTextToBeReplaced", "", "Wich part of the file name should be replaced",],
    ["--shouldReplaceFileContent", "", "Should or not CLI replace a file content",],
    ["--textToBeReplaced", "", "Text to be replaced separated by a search and replace separator",],
    ["--replaceTextWith", "", "Text to be used for search and replace separated by a separator",],
    ["--searchAndReplaceSeparator", "", "Custom separator for search and replace"],
    ["--version", "-v", "Show the current version of the package"],
    ["--debug", "", "Show additional logs"],
  ];

  table.push(...rows);

  console.log(table.toString());
```
When we execute the help command (`cfft --help`), we should see the following result (resize the terminal if it doesn’t look the same):

![The help command result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/olvpx8y1lqnryts7upw4.png)

> Note: Check [/src/help.ts](https://github.com/dus4nstojanovic/create-files-from-template-cli/blob/master/src/help.ts) for more info on GitHub.

###Test your code locally

With all these features, our CLI is almost ready. But during our development, before publishing it globally, we would like to test it.

This is possible by adding the following lines to the package.json file (See [package.json](https://github.com/dus4nstojanovic/create-files-from-template-cli/blob/master/package.json)):

```json
"bin": {
    "cfft-dev": "dist/index.js"
 }
```

**bin** – is where you add your global commands.  We basically tell Node that when we execute the `cfft-dev` command, it should run the `dist/index.js` file (which is a file we create on the application build). 

The very important step is to add the following line of code to your executable file (*index.ts*) as the first line:

```javascript
#!/usr/bin/env node
```
This line tells Node that this is a node executable file.

The next step is to install the command locally. Navigate the terminal to the location of your package.json, and execute the following command:

```sh
npm link
```

Voila! You can test your CLI locally now:

```sh
cfft-dev --version
```

###Publish to NPM

Last, but not Least! Our CLI is finished, and we want to allow other people to use it. Let’s publish it to NPM.

In the */dist* folder create another package.json. Fill in all necessary fields - *author*, *repository*, add *keywords*, *version*, description, etc. See [package.json](https://github.com/dus4nstojanovic/create-files-from-template-cli/tree/master/dist)

But also, it is very important to add the bin command that will be registered when people install your CLI globally (or using npx).

```json
"bin": {
    "cfft": "index.js"
 }
```

By doing so, our CLI is ready to be published. Navigate to your */dist* folder, execute the `npm publish` and feel proud of yourself! Congrats! 💪 🎉 (Note: follow the documentation on [npm](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages) on how to do it).

-------
Thank you! 🙂