How to create `npm create` package
Tags: npm, javascript, tutorial, node
Published: 2023-09-30 16:32:32

## TL;DR

This past few months I'm pretty active in creating some template code and researching the best project structure for different types of project at my workplace. 

But then I realize that there are legacy template as well that we still use and maintain and these templates began to hard to manage and find, especially for new people.

Then we found an idea to create a space to put all our templates, and also maintain them all in one place.

## npm create Comes to Mind

I guess we are already familiar with `npm create <something>` command, usually being used to scaffold a project with existing template like Create Vite App, or Create React App. So we adapt this concept to manage all of our templates.

## Project Setup

First, create a folder and name it `create-template` and cd into it
```bash
$ mkdir create-template && cd create-template
```

Then run this command to create a `package.json` file
```bash
$ npm init
```

Now open the package.json file and modify it a little bit so it look like this

```json
{
  "name": "create-template",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "bin": {
    "create-template": "index.js",
    "ctpl": "index.js"
  },
  "engines": {
    "node": "^18.0.0 || >=20.0.0"
  },
  "files": [
    "index.js",
    "templates/*"
  ],
}
```

The bin property is very important for when we write `npm create template` command, it will execute the specified file. In our case we want it to execute index.js file.

Next step is to install one package to prompt user to provide us the information needed.

```bash
$ npm i -D prompts
```

Let's create 2 folders, `src` and `templates` where `src` will contain our logics and `templates` will contain our templates. And also create an index.js file inside the `src` folder.

```bash
$ mkdir src
$ mkdir templates
$ touch src/index.js
```

## The Logics

![Logic](https://media.tenor.com/xo1JUb77UUoAAAAC/logic-think.gif)

Now after we have finished our preparation, we will then breakdown the flow or the logic of the CLI.

1. We have to prompt the user about what template they want to use and what would they name the project.

2. Create a directory named after the user's project name and don't forget to check if the target directory already exist, if it exist then abort the process.

3. Copy the selected template to the target directory

4. Change the package.json name property to the project name

5. Remove unecessary file (If exist)

## Let's Code!

![Coding](https://media.tenor.com/GfSX-u7VGM4AAAAC/coding.gif)

First, let's define an array of templates that we have that work as an options for the user.

```javascript
const templates = [
  {
    value: "template-1",
    title: "Template 1",
    description: "This is template 1",
  },
  {
    value: "template-2",
    title: "template-2",
    description: "This is template 2",
  }
];
```

Now let's create an [IIFE](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) along with the prompts inside it

```javascript
import prompts from "prompts";

(async () => {
  try{
    const response = await prompts([
      {
        type: "select",
        name: "template",
        message: "Select template",
        choices: templates,
      },
      {
        type: "text",
        name: "projectName",
        message: "Enter your project name",
        initial: "my-project",
        format: (val) => val.toLowerCase().split(" ").join("-"),
        validate: (val) =>
          projectNamePattern.test(val)
            ? true
            : "Project name should not contain special characters except hyphen (-)",
      },
    ]);
    const { projectName, template } = response;
  }
  catch(err){
    console.log(err.message);
  }
})()
```

If you run the script, then you should see a prompt asking for template choice and project name. Now let's go to the second logic.

We have to get the target directory path and our templates directory path

```javascript
import { fileURLToPath } from "node:url";
import path from "node:path";


...
    const targetDir = path.join(cwd, projectName);
    const sourceDir = path.resolve(
      fileURLToPath(import.meta.url),
      "../../templates",
      `${template}`
    );
...
```

Also we have to check if the target directory is already exist or not

```javascript
import fs from "fs";

if (!fs.existsSync(targetDir)) {
      // Copying logic
      console.log("Target directory doesn't exist");
      console.log("Creating directory...");
      fs.mkdirSync(targetDir, { recursive: true });
      console.log("Finished creating directory");
      await copyFilesAndDirectories(sourceDir, targetDir);
      await renamePackageJsonName(targetDir, projectName);
      console.log(`Finished generating your project ${projectName}`);
      console.log(`cd ${projectName}`);
      console.log(`npm install`);
    } else {
      throw new Error("Target directory already exist!");
    }
```

Now we will write the logic for `copyFilesAndDirectories` and `renamePackageJsonName` function

```javascript
import {
  writeFile,
  lstat,
  readdir,
  mkdir,
  copyFile,
  readFile,
} from "fs/promises";

const copyFilesAndDirectories = async (source, destination) => {
  const entries = await readdir(source);

  for (const entry of entries) {
    const sourcePath = path.join(source, entry);
    const destPath = path.join(destination, entry);

    const stat = await lstat(sourcePath);

    if (stat.isDirectory()) {
      // Create the directory in the destination
      await mkdir(destPath);

      // Recursively copy files and subdirectories
      await copyFilesAndDirectories(sourcePath, destPath);
    } else {
      // Copy the file
      await copyFile(sourcePath, destPath);
    }
  }
};

const renamePackageJsonName = async (targetDir, projectName) => {
  const packageJsonPath = path.join(targetDir, "package.json");
  try {
    const packageJsonData = await readFile(packageJsonPath, "utf8");
    const packageJson = JSON.parse(packageJsonData);
    packageJson.name = projectName;
    await writeFile(
      packageJsonPath,
      JSON.stringify(packageJson, null, 2),
      "utf8"
    );
  } catch (err) {
    console.log(err.message);
  }
};
```

Well done! We have done all the logics. Now to test it out, run the script and fill the prompts. If it works as intended, let's go go the next step.

## Making it Executable

In order for this tool to be executable, we need to create an index.js file at the root level.

```bash
$ touch index.js
```

This file doesn't contain much, only importing our logic from `src/index.js` and adding a shebang

```javascript
#!/usr/bin/env node
import "./src/index.js";
```

To test it out, we have to symlink our package first and check if it's installed globally

```bash
$ npm link && npm list -g
```

If done, then run the command.

```bash
$ npx create-template
```

If everything works correctly then, Congratulations!!

## Deploy to NPM

If you are interested in deploying the package to NPM, unfortunately this post won't cover this topic but you should be able to do it by reading this [documentation](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages)

That brings us to the end of this article. If you enjoy this article, don't forget to comment what you liked and didn't like about this tutorial and if there's something you know that I should have included in this article, do let me know in the comments down below.

Dadah~ 👋