Build a custom web application generator with yeoman

Background

At the University of Central Missouri our web application team currently uses a home-grown framework built to help us speed up our development and maintenance time by providing the common, standardized pieces we use in every one of our applications.

The original version consisted of a git repository with a readme.txt file with instructions on how to search and replace certain placeholder text for titles and variable, file, and class names.

When I discovered Grunt I created a script to ask for certain questions in lieu of the search and replace method mentioned above. This worked well and sped up our application start up time tremendously; however, there were certain conditional pieces of code in our applications that should be included or not based on answers in the Grunt script. For example, we use two different Database Management Systems (DBMS) and have certain code needed depending on which is chosen for that project. I was unable to find a good solution for this using Grunt.

Our Basic Yeoman Setup

After some time passed, I came across Yeoman and determined that it would handle the conditional case mentioned above and would allow for more flexibility in the future. I have outlined the main pieces of how I set it up below.

Dependencies / package.json

Besides the yeoman-generator dependency, our application generator also leverages random.org’s API to generate unique strings using this npm library. I have included our package.json file in its entirety below.

{
  "name": "generator-custom-webapp",
  "version": "1.3.1",
  "description": "A custom webapp generator.",
  "files": [
    "app"
  ],
  "keywords": [
    "yeoman-generator"
  ],
  "dependencies": {
    "yeoman-generator": "^0.20.3",
    "random-org": "^0.1.3"
  }
}

 app/index.js

We start by requiring the random-org generator and then instantiating it using our API key we received from Random.org.

'use strict';
var RandomOrg = require('random-org');
var generators = require( 'yeoman-generator' );
module.exports = generators.Base.extend({
    prompting: function () {
        var done = this.async();
        this.random = new RandomOrg( { apiKey: 'your_api_key_here' } );

Prompts

Next we set the prompts we want for our application details. For example, we ask for an application abbreviation which is used to build portions of class names and variables throughout the application. Additionally, we have a checkbox prompt for DBMS type which is used in the templates to conditionally pull the correct database files.

Example prompts:

        {
            type: 'input',
            name: 'application_abbr',
            message: 'Your application abbreviation',
            default: this.appname
        },
        {
            type: 'checkbox',
            name: 'db_type',
            message: 'Database Type',
            choices: [{
                name: 'mysql',
                value: 'includeMysql',
                checked: true
            },{
                name: 'postgre',
                value: 'includePostgre',
                checked: false
            }]
        }

Example template logic:

<?php
<% if (includePostgre) { -%>
 // instantiate Postgre db class
 $dbClass = new <%= application_abbr %>PostgreClass();
<% } %>
<% if (includeMysql) { -%>
  // instantiate mysql db class
  $dbClass = new <%= application_abbr %>MysqlClass();
<% } %>

Notice that the checkbox value is used in the template if statements and the application abbreviate value is used in the  class instantiation call.

While still inside of the prompting function, but after the prompts, we access the answers given and set them into a variable (this.all_app_vars) that will be used in the writing function.

            var db_type = answers.db_type;
            function hasDbtype(dtype) {
                return dbtype && dbtype.indexOf(dtype) !== -1;
            }

            if( hasDbtype( 'includeMysql' ) ) {
                db_name = 'mysql';
            }

            if( hasDbtype( 'includePostgre' ) ) {
                db_name = 'postgre';
            }

            this.all_app_vars = {
                application_abbr: answers.application_abbr,
                includeMysql: hasDbtype( 'includeMysql' ),
                includePostgre: hasDbtype( 'includePostgre' ),
            };

Generating the files

In the writing function we call the Random.org API to get the random strings, display information from Random.org so we don’t violate the API’s terms, and call the function that copies the templates to our destination (the template variables are parsed during the copy function).

writing: {
        app: function () {
            this.random.generateStrings({n: 2, length: 15, characters: "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}).then(function(result) {
                console.log("Bits used: " + result.bitsUsed);
                console.log("Bits left: " + result.bitsLeft);
                console.log("Requests left for today: " + result.requestsLeft);
                console.log("DO NOT Run Another request for the next: " + result.advisoryDelay + " milliseconds");
                for(var i=0; i<result.random.data.length; i++) {
                    switch (i) {
                        case 0:
                            this.all_app_vars.secret_key = result.random.data[i];
                            break;
                        case 1:
                            this.all_app_vars.special_salt = result.random.data[i];
                            break;
                        default:
                            break;
                    }
                }
                this.fs.copyTpl(this.templatePath(), this.destinationPath(), this.all_app_vars);
            }.bind(this));
        }
    },

Example index.js file

'use strict';
var RandomOrg = require('random-org');
var generators = require( 'yeoman-generator' );
module.exports = generators.Base.extend({
    prompting: function () {
        var done = this.async();
        this.random = new RandomOrg( { apiKey: 'your_api_key_here' } );
        var prompts = [
        {
            type: 'input',
            name: 'application_abbr',
            message: 'Your application abbreviation',
            default: this._helpers.abbrFromString(this.appname)
        },
        {
            type: 'checkbox',
            name: 'db_type',
            message: 'Database Type',
            choices: [{
                name: 'mysql',
                value: 'includeMysql',
                checked: true
            },{
                name: 'postgre',
                value: 'includePostgre',
                checked: false
            }]
        }
        ];
        this.prompt(prompts, function ( answers ) {
            var db_type = answers.db_type;
            function hasDbtype(dtype) {
                return dbtype && dbtype.indexOf(dtype) !== -1;
            }
 
            if( hasDbtype( 'includeMysql' ) ) {
                db_name = 'mysql';
            }
 
            if( hasDbtype( 'includePostgre' ) ) {
                db_name = 'postgre';
            }
 
            this.all_app_vars = {
                application_abbr: answers.application_abbr,
                includeMysql: hasDbtype( 'includeMysql' ),
                includePostgre: hasDbtype( 'includePostgre' ),
            };
            done();
        }.bind(this));
    },
 
    writing: {
        app: function () {
            this.random.generateStrings({n: 2, length: 28, characters: "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}).then(function(result) {
                console.log("Bits used: " + result.bitsUsed);
                console.log("Bits left: " + result.bitsLeft);
                console.log("Requests left for today: " + result.requestsLeft);
                console.log("DO NOT Run Another request for the next: " + result.advisoryDelay + " milliseconds");
                for(var i=0; i<result.random.data.length; i++) {
                    switch (i) {
                        case 0:
                            this.all_app_vars.secret_key = result.random.data[i];
                            break;
                        case 1:
                            this.all_app_vars.special_salt = result.random.data[i];
                            break;
                        default:
                            break;
                    }
                }
                this.fs.copyTpl(this.templatePath(), this.destinationPath(), this.all_app_vars);
            }.bind(this));
        }
    },
 
    _helpers: {
        upperWords: function (sampleString) {
            return sampleString.replace(/\w\S*/g, function(txt) {return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
        },
 
        abbrFromString: function (sampleString) {
            var abbr = sampleString.substring(0, 2);
            return this.makeAbbrSafe(abbr);
        },
 
        makeAbbrSafe: function (abbrString) {
            var abbr = abbrString.replace(/[\W_]+/g, '_').replace(/^(\d)/, '_$1');
            return abbr;
        },
 
        createClassAbbr: function (abbrString) {
            var classAbbr = abbrString.charAt(0).toUpperCase() + abbrString.slice(1);
            return classAbbr;
        },
 
        getCurrentDate: function () {
            var date = new Date();
            return date.toString();
        }
    }
});

Hopefully that will give you an idea of how to set up your own custom generator, if you have any questions please leave a reply in the comments and I’ll do my best to get them answered.

Leave a Reply

Your email address will not be published. Required fields are marked *