DEV Community

Cover image for Build your own Project Template Generator
Duwaine
Duwaine

Posted on

Build your own Project Template Generator

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

Starter Setup

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

npm start outcome

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

Oldest comments (1)

Collapse
 
vitkuz profile image
vitkuz

Amazing tutorial. Thank you!