How to create `npm create` package
Mikhael Esa
Posted on September 30, 2023
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
Then run this command to create a package.json
file
$ npm init
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/*"
],
}
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
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
The Logics
Now after we have finished our preparation, we will then breakdown the flow or the logic of the CLI.
We have to prompt the user about what template they want to use and what would they name the project.
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.
Copy the selected template to the target directory
Change the package.json name property to the project name
Remove unecessary file (If exist)
Let's Code!
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",
}
];
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);
}
})()
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}`
);
...
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!");
}
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);
}
};
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
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";
To test it out, we have to symlink our package first and check if it's installed globally
$ npm link && npm list -g
If done, then run the command.
$ 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
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~ 👋
Posted on September 30, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.