Build your own custom cli project template generator from anywhere on your machine. I have used the guidelines from this article How to build your own project templates using Node CLI and typescript but have run into a few problems so I decided to sort them out myself and create a new article.
Why do I want to create my own template generator?
From time to time you want to create a new project but it is based on something you have set up before. Let's say I need React with Node but I forget how I implemented everything in previous projects so instead of sourcing through old projects why not create a template for that and use it from this template generator?
If you just want to skip ahead and not build your own here is a repo to the Project Template Generator
Lets get started
1. Create new typescript project
1. create a new project folder (mkdir PROJECT_NAME & cd PROJECT NAME)
2. run npm init (to initialize a new node project)
3. run npm add -D typescript ts-node nodemon
- ts-node is used to run typescript without compiling
- nodemon is used to run/restart node automatically when files changed
4. run npx tsc --init
5. adjust tsconfig.json to the following
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"declaration": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
}
}
6. create folder src and index.ts file
2. Add sample-project to your main template generator project
1. create a the following folder tree inside your project: src/templates/sample-project.
2. inside the sample-projects folder create a package.json file that contains the following:
{
"name": "sample-project",
"version": "1.0.0",
"license": "MIT"
}
Your structure now should look something like this
3. Add the following at the top of "src/index.ts"
1. #!/usr/bin/env node
This is known as "shebang" this tells node to run the typescript code.
4. Now lets read the templates folder as choices in the cli
1. run npm add -D @types/node @types/inquirer
2. run npm add inquirer
3. update src/index.ts
import * as fs from 'fs';
import * as path from 'path';
import * as inquirer from 'inquirer';
import chalk from 'chalk';
const CHOICES = fs.readdirSync(path.join(__dirname, 'templates'));
const QUESTIONS = [
{
name: 'template',
type: 'list',
message: 'What project template would you like to use?',
choices: CHOICES
},
{
name: 'name',
type: 'input',
message: 'New project name?'
}];
inquirer.prompt(QUESTIONS)
.then(answers => {
console.log(answers);
});
4. to test update package.json script
"scripts": {
"start": "ts-node src/index.ts"
}
and run npm start
There will be more templates in the list when you add your own in src/templates
5. Use the input options
1. update src/index.ts with the following
2. export interface CliOptions {
projectName: string
templateName: string
templatePath: string
tartgetPath: string
}
const CURR_DIR = process.cwd();
inquirer.prompt(QUESTIONS)
.then(answers => {
const projectChoice = answers['template'];
const projectName = answers['name'];
const templatePath = path.join(__dirname, 'templates', projectChoice);
const tartgetPath = path.join(CURR_DIR, projectName);
const options: CliOptions = {
projectName,
templateName: projectChoice,
templatePath,
tartgetPath
}
console.log(options);
});
6. Create your project folder
At the question New project name? create a new folder "projectName" in the root directory.
function createProject(projectPath: string) {
if (fs.existsSync(projectPath)) {
console.log(chalk.red(`Folder ${projectPath} exists. Delete or use another name.`));
return false;
}
fs.mkdirSync(projectPath);
return true;
}
In case of failure stop the function
inquirer.prompt(QUESTIONS)
.then(answers => {
....
if (!createProject(tartgetPath)) {
return;
}
});
7. Copy files and folders from chosen template to new project
1. Add to src/index.ts
2. // list of file/folder that should not be copied
const SKIP_FILES = ['node_modules', '.template.json'];
function createDirectoryContents(templatePath: string, projectName: string) {
// read all files/folders (1 level) from template folder
const filesToCreate = fs.readdirSync(templatePath);
// loop each file/folder
filesToCreate.forEach(file => {
const origFilePath = path.join(templatePath, file);
// get stats about the current file
const stats = fs.statSync(origFilePath);
// skip files that should not be copied
if (SKIP_FILES.indexOf(file) > -1) return;
if (stats.isFile()) {
// read file content and transform it using template engine
let contents = fs.readFileSync(origFilePath, 'utf8');
// write file to destination folder
const writePath = path.join(CURR_DIR, projectName, file);
fs.writeFileSync(writePath, contents, 'utf8');
} else if (stats.isDirectory()) {
// create folder in destination folder
fs.mkdirSync(path.join(CURR_DIR, projectName, file));
// copy files/folder inside current folder recursively
createDirectoryContents(path.join(templatePath, file),
path.join(projectName, file));
}
});
}
Add the following code after you create the tempalte
....
if (!createProject(tartgetPath)) {
return;
}
createDirectoryContents(templatePath, projectName);
....
8. Testing the program as a CLI
Install tool "shx" for building script
1. Run npm add -D shx
2. Add the following build script to package.json
"build": "tsc && shx rm -rf dist/templates && shx cp -r src/templates dist"
3. npm run build
4. Add bin to package.json
"bin": {
"template-generator": "./dist/index.js"
}
5. Register "template-generator" as a command line interface
run npm link
If successful, you can run the command "template-generator" anywhere on your machine. (Make sure you have permission to read/write files)
9. Final step: Rename project as new project created by input
So now you can choose a template from the given questions list and then input a new project name but the template files that are being copied over are exactly the same like the project name in the new package.json and we want to automate that.
1. update template "src/templates/sample-project/package.json" with a placholder name
{
"name": "<%= projectName %>",
"version": "1.0.0",
....
}
2. npm add ejs
add -D @types/ejs
3. update src/utils/template.ts to render template under utils
import * as ejs from 'ejs';
export interface TemplateData {
projectName: string
}
export function render(content: string, data: TemplateData) {
return ejs.render(content, data);
}
4. Add code to transform the content inside "src/index.ts" function "createDirectoryContents"
if (stats.isFile()) {
// read file content and transform it using template engine
let contents = fs.readFileSync(origFilePath, 'utf8');
contents = template.render(contents, { projectName });
}
5. run npm build and then generate-template to test that the new project name is inserted in the "<%= projectName %>" placeholder.
Your project template generator should now be complete.
Here is the full src/index.ts file incase you have missed something
#!/usr/bin/env node
import * as fs from 'fs';
import * as path from 'path';
import * as inquirer from 'inquirer';
import chalk from 'chalk';
import * as template from './utils/template';
import * as shell from 'shelljs';
const CHOICES = fs.readdirSync(path.join(__dirname, 'templates'));
const QUESTIONS = [
{
name: 'template',
type: 'list',
message: 'What template would you like to use?',
choices: CHOICES
},
{
name: 'name',
type: 'input',
message: 'Please input a new project name:'
}];
export interface CliOptions {
projectName: string
templateName: string
templatePath: string
tartgetPath: string
}
const CURR_DIR = process.cwd();
inquirer.prompt(QUESTIONS).then(answers => {
const projectChoice = answers['template'];
const projectName = answers['name'];
//@ts-ignore
const templatePath = path.join(__dirname, 'templates', projectChoice);
//@ts-ignore
const tartgetPath = path.join(CURR_DIR, projectName);
const options: CliOptions = {
//@ts-ignore
projectName,
//@ts-ignore
templateName: projectChoice,
templatePath,
tartgetPath
}
if (!createProject(tartgetPath)) {
return;
}
//@ts-ignore
createDirectoryContents(templatePath, projectName);
postProcess(options);
});
function createProject(projectPath: string) {
if (fs.existsSync(projectPath)) {
console.log(chalk.red(`Folder ${projectPath} exists. Delete or use another name.`));
return false;
}
fs.mkdirSync(projectPath);
return true;
}
const SKIP_FILES = ['node_modules', '.template.json'];
function createDirectoryContents(templatePath: string, projectName: string) {
// read all files/folders (1 level) from template folder
const filesToCreate = fs.readdirSync(templatePath);
// loop each file/folder
filesToCreate.forEach(file => {
const origFilePath = path.join(templatePath, file);
// get stats about the current file
const stats = fs.statSync(origFilePath);
// skip files that should not be copied
if (SKIP_FILES.indexOf(file) > -1) return;
if (stats.isFile()) {
// read file content and transform it using template engine
let contents = fs.readFileSync(origFilePath, 'utf8');
contents = template.render(contents, { projectName });
// write file to destination folder
const writePath = path.join(CURR_DIR, projectName, file);
fs.writeFileSync(writePath, contents, 'utf8');
} else if (stats.isDirectory()) {
// create folder in destination folder
fs.mkdirSync(path.join(CURR_DIR, projectName, file));
// copy files/folder inside current folder recursively
createDirectoryContents(path.join(templatePath, file), path.join(projectName, file));
}
});
}
function postProcess(options: CliOptions) {
const isNode = fs.existsSync(path.join(options.templatePath, 'package.json'));
if (isNode) {
shell.cd(options.tartgetPath);
const result = shell.exec('npm install');
if (result.code !== 0) {
return false;
}
}
return true;
}
and a link to the full project to use : Project Template Generator
Top comments (1)
Amazing tutorial. Thank you!