DEV Community

Cover image for How to create `npm create` package
Mikhael Esa
Mikhael Esa

Posted on • Updated on

How to create `npm create` package

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



$ mkdir create-template && cd create-template


Enter fullscreen mode Exit fullscreen mode

Then run this command to create a package.json file



$ npm init


Enter fullscreen mode Exit fullscreen mode

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



{
  "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/*"
  ],
}


Enter fullscreen mode Exit fullscreen mode

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.



$ npm i -D prompts


Enter fullscreen mode Exit fullscreen mode

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.



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


Enter fullscreen mode Exit fullscreen mode

The Logics

Logic

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

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



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


Enter fullscreen mode Exit fullscreen mode

Now let's create an IIFE along with the prompts inside it



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);
  }
})()


Enter fullscreen mode Exit fullscreen mode

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



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}`
    );
...


Enter fullscreen mode Exit fullscreen mode

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



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!");
    }


Enter fullscreen mode Exit fullscreen mode

Now we will write the logic for copyFilesAndDirectories and renamePackageJsonName function



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);
  }
};


Enter fullscreen mode Exit fullscreen mode

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.



$ touch index.js


Enter fullscreen mode Exit fullscreen mode

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



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


Enter fullscreen mode Exit fullscreen mode

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



$ npm link && npm list -g


Enter fullscreen mode Exit fullscreen mode

If done, then run the command.



$ npx create-template


Enter fullscreen mode Exit fullscreen mode

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

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~ 👋

Top comments (0)