Create Your Own Project, Pattern, Component And Files Generator: Yeoman Vs Plop

A yeoman and plop.js tutorial for getting started generating projects and files
Scaffold & bootstrap any kind of app (Web, Java, Spring Boot, Python, C#, Node.js), generate a project section or a group of files that follow a common pattern from the CLI and get started authoring custom template based generators with Yeoman (with ejs.js) and Plop.js (with Handlebars.js)

Yeoman

Yeoman is a scaffolding tool that helps you kickstart your new projects by providing a generator ecosystem that can be launched with the command yo. It is most commonly used to generate entire projects, but can also generate any kind of files.

Installation

  1. Node.js (with npm included) is required. You can download the latest version from the following link. (If you encounter any difficulties while using the yo command with Windows 10, install the version 8 of node instead from the following link)
  2. From the command line, run npm install -g yo
    Yeoman - 1. Yo Installation
  3. Run npm install -g generator-generator
    Yeoman - 2. generator-generator

Getting started

Run the yo generator command at the folder where you want to generate the generator project. You will be prompted to input the generator name and other details.

Yeoman - 3. Project generator (1)
Yeoman - 3. Project generator (2)

The directory tree of the generator project will look like this:

Yeoman - 4. Generator project structure

The generators\app\index.js file is the main entry of the generator.

By default, Bower is assumed to be installed and to be used to manage dependencies. To disable this behavior, you can set the arguments of the installDependencies method call of the generator's index.js to the following:

install() {
    this.installDependencies({ bower: false });
}

To be able to launch this generator from any working directory of your computer, run npm link from the root of the generator project

Yeoman - 5. npm link

(You can also publish this module to an npm registry so that it can be used by other users)

You can now test your your generator from any directory using the command yo your-generator-name (without the generator- prefix):

Yeoman - 6. yo angular-node-typescript

You will have a dummyfile.txt file generated in your current working directory.

Sub-generators

You can implement in the same generator project additional generators that can be launched with the yo your-generator-name:sub-generator-name command.

They can be generated by running yo generator:subgenerator <name> at the root folder of the generator project

Yeoman - 7. Generate sub-generator

A dummyfile.txt file will be generated when running the yo your-generator-name:sub-generator-name command:

Yeoman - 8. Run sub-generator

Basic generator implementation

You can remove the welcome banner by deleting the following code from the prompting method

    // Have Yeoman greet the user.
    this.log(
      yosay(
        `Welcome to the wonderful ${chalk.red(
          "generator-your-generator-name"
        )} generator!`
      )
    );

To prompt the user for the project's name, you can replace the prompts array inside the prompting method with the following value:

    const prompts = [
      {
        name: 'name',
        message: 'Your project name',
        default: 'generated-app'
      }
    ];

By default, yeoman generates the files in the current working directory. If you want to generate a folder instead where the template files will be copied:

  1. Install the mkdirp dependency with npm i -S mkdirp
  2. In the generator's index.js, add the following require call:
     const mkdirp = require('mkdirp');
  3. Implement a method before the writing method with the following code:
         default() {
             mkdirp(this.props.name);
             this.destinationRoot(this.destinationPath(this.props.name));
         }

You can now replace the your generator's templates folder with the files you want to generate.

Yeoman - 9. Template files

Yeoman doesn't directly modify the filesystem, but does all the file manipulation in memory and only write the resulting file tree at the end. this.fs is a provided in-memory filesystem edition helper based on MemFsEditor that is used to copy files and folders to the new destination. You can check all the available methods in this page.

this.fs.copy can be called to copy files and folders. The templatePath and destinationPath methods are used to join a path to the template source root and destination root respectively.

The template copying logic can be implemented inside the writing method:

  writing() {
    this.fs.copy(this.templatePath('*'), this.destinationPath());
    this.fs.copy(this.templatePath('.*'), this.destinationPath());
    this.fs.copy(this.templatePath('src'), this.destinationPath('src'));
    this.fs.copy(this.templatePath('server'), this.destinationPath('server'));
    this.fs.copy(this.templatePath('e2e'), this.destinationPath('e2e'));
  }

To insert variables inside files, the this.fs.copyTpl method can be used. It supports the ejs templating system.

In the current example, the project name can be inserted inside a template file with: <%= name %>.

// package.json
{
  "name": "<%= name %>",
  "version": "1.0.0"
  // ...
}

To copy a package.json template file that includes the name variable:

  writing() {
    // ...
    this.fs.copyTpl(
      this.templatePath('package.json'),
      this.destinationPath('package.json'),
      this.props // template variables
    );
  }

Check that the files are copied and that the variables are well replaced after executing the command yo your-generator-name

Yeoman - 10. Project generation

Yeoman - 11. Generated project

This is the final index.js code of this basic generator implementation:

"use strict";
const Generator = require("yeoman-generator");
const mkdirp = require("mkdirp");

module.exports = class extends Generator {
  prompting() {
    const prompts = [
      {
        name: "name",
        message: "Your project name",
        default: "generated-app"
      }
    ];

    return this.prompt(prompts).then(props => {
      // To access props later use this.props.someAnswer;
      this.props = props;
    });
  }

  default() {
    mkdirp(this.props.name);
    this.destinationRoot(this.destinationPath(this.props.name));
  }

  writing() {
    this.fs.copy(this.templatePath('*'), this.destinationPath());
    this.fs.copy(this.templatePath('.*'), this.destinationPath());
    this.fs.copy(this.templatePath('src'), this.destinationPath('src'));
    this.fs.copy(this.templatePath('server'), this.destinationPath('server'));
    this.fs.copy(this.templatePath('e2e'), this.destinationPath('e2e'));
    this.fs.copyTpl(
      this.templatePath('package.json'),
      this.destinationPath('package.json'),
      this.props
    );
  }

  install() {
    this.installDependencies({ bower: false });
  }
};

References and resources

Plop.js

Plop.js is described as a "micro-generator framework". It's better suited to generate smaller parts of existing projects or any kind of text files in a consistent way. It provides a generator ecosystem based on Inquirer.js prompts and Handlebar.js templates.

Contrary to Yeoman, it can't be used to generate entire projects. In return, It has more adapted features to generate or modify files inside existing projects.

Installation

  1. Node.js (with npm included) is required. You can download the latest version from the following link.
  2. From the command line, run npm install -g plop
    Plop - 1. Installation
  3. (Optional) If you have a Node.js project, plop can be saved as a dev dependency by running the following command in the root directory of the project npm install --save-dev plop

Getting started

Create a plopfile.js file at the root directory of the project where you want to generate files with the following content:

module.exports = function (plop) {
    plop.setGenerator('my-generator', {
        description: 'My first plop generator',
        prompts: [],
        actions: [{
            type: 'add',
            path: 'src/main.js',
            template: 'console.log("Hello, World!");'
        }]
    });
};

Then run the plop my-generator command from the root directory (or subdirectory) of the project where you want to generate files.

Example with empty project directory containing just the plopfile.js file:

Plop - 2. First generator

The src/main.js file is generated with the console.log("Hello, World!"); content:

Plop - 3. First generated file

Basic generator implementation

In this example, we will be generating controller classes for a Java Spring Boot application (the code was initialized using https://start.spring.io/ with the Web dependency)

Plop - 4. Initial project structure

Add a plopfile.js file to the root of the application with the following content:

module.exports = function (plop) {
    plop.setGenerator('controller', {
        description: 'Spring Boot Rest Controller',
        prompts: [{
            type: 'input',
            name: 'name',
            message: 'Controller name'
        }],
        actions: [{
            type: 'add',
            path: 'src/main/java/com/example/moviesapp/controller/{{pascalCase name}}Controller.java',
            templateFile: 'plop-templates/controller.hbs'
        }]
    });
};

The prompts array holds all the variable definition objects that will be requested as user input using Inquirer.js or provided as command line arguments. The variable definition object format is described here.

The actions array holds the actions that the generator will execute to add and modify files to your project. The built-in actions and their formats are detailed here.

All the variables collected from the prompts are made available by Plop.js to be used in the template files (file name & content). The syntax of these templates is based on Handlebar.js.

Let's create a plop-templates folder at the root of the project to hold the templates that will be used by the plop generator, and a controller.hbs file inside it with the following content:

package com.example.moviesapp.controller;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
public class {{pascalCase name}}Controller {

    @RequestMapping("/{{dashCase name}}")
    public String index() {
        return "{{pascalCase name}}Controller works!";
    }

}

Notice that pascalCase and dashCase helper functions were applied to the variable (inside this template & in the plopfile.js file) to change it's case.

More built-in helpers can be found here.

To generate a new controller in the project using user input, run the plop controller command:

Plop - 5. Generate with user prompt

A new controller can also be generated without user input by running the command plop controller <controller-name>:

Plop - 6. Generate without user prompt

The generated controller file:

Plop - 7. Generated controller file

Share generators

The previous example can be refactored to make the controller generator available to any project.

If not already done, set the NODE_PATH environment variable to your global node modules location. Default path examples:

  • Linux:
    /usr/lib/node_modules
  • Windows:
    C:\Users\USER_NAME\AppData\Roaming\npm\node_modules

In your workspace, create a directory that will hold all your generators implementation.

Create a package.json file in the this directory with the following contents:

{
  "name": "plop-pack-generators",
  "version": "1.0.0",
  "main": "plopfile.js"
}

Then move the plopfile.js file and plop-templates folder from the previous example to this plop generators project directory.

Plop - 8. Shared generator project

To be able to load this generator from any working directory of your computer, run npm link from the root of the generator project

Plop - 9. npm link my-plop-generators

(You can also publish this module to an npm registry so that it can be used by other users)

To correctly resolve the templates path, and customize the generator with the current project's specificities (app's root package for example), change the plopfile.js of the plop generators project with the following content:

const path = require('path');
const templatesPath = path.resolve(__dirname, 'plop-templates')

module.exports = function (plop, config) {
    const package = config && config.package
    const packagePath = package ? (package.replace(/\./g, "/") + "/") : ""
    plop.setGenerator('controller', {
        description: 'Spring Boot Rest Controller',
        prompts: [{
            type: 'input',
            name: 'name',
            message: 'Controller name'
        }],
        actions: [{
            type: 'add',
            path: `src/main/java/${packagePath}controller/{{pascalCase name}}Controller.java`,
            templateFile: `${templatesPath}/controller.hbs`,
            data: config
        }]
    });
};

The following plopfile.js file must be be created in the root directory of the projects where you want to use the generators:

module.exports = function (plop) {
    require('plop-pack-generators')(plop, {
        package: "your.app.package"
    });
};

Plop - 10. Generate with shared generator

Plop - 11. Generated controller and local plopfile

References and resources