Writing Yeoman generators in ES6

James Allardice on


In a recent article I explored how it's possible to use ES2015 (formerly ES6) for modules published to npm, thanks to source-to-source transpilers such as Babel. Further to this, at Mammal we are starting to make the transition to ES6 for new projects. As part of this transition we wanted to build a Yeoman generator to speed up the initial project setup process. Since the generator will be outputting ES6, why not write it in ES6?

Note. This is only possible with the yeoman-generator package from version 0.18.10 onwards. This is due to a bug in prior versions. To resolve the issue we opened a pull request which was merged and released as part of 0.18.10.

Yeoman generators in ES5

Before we look at ES6, here's a reminder of how Yeoman generators can be created in ES5, adapted from Yeoman's own guide:

var Base = require('yeoman-generator').Base;

module.exports = Base.extend({

  // Override the Base constructor to provide custom options
  constructor: function () {
    Base.apply(this, arguments);
    this.argument('appname');
  },

  // Add a generator method that will run after construction
  method1: function () {
    console.log('The name is:', this.appname);
  },
});

If you're familiar with ES6 you may notice that semantically the above code could be represented with the new class and extends syntax, which is just sugar around the prototypal inheritance model. The ES5 version is already halfway there. It relies upon a property called constructor and hides the inheritance stuff behind an extend method.

Switching to ES6

Let's rewrite the above example in ES6. We're using a few new features but pay particular attention to the class and extends syntax.

import { Base } from 'yeoman-generator';

export default class MyGenerator extends Base {

  constructor( ...args ) {
    super(...args);
    this.argument('appname');
  }

  method1() {
    console.log(`The name is: ${ this.appname }`);
  }
}

So far so good. The ES6 version is no more concise but it uses native language features to convey meaning instead of the custom extend implementation used in the ES5 code.

Yeoman "priorities"

Yeoman supports grouping of methods into priorities by extending the base generator with properties whose values are simple objects rather than functions. When a generator is run Yeoman iterates over the properties attached to the generator prototype. If a value is a function (as with method1 in the previous examples) then that function is executed. If the value is an object Yeoman repeats the process for that object.

The ES6 class syntax doesn't support proper instance properties (as opposed to methods) but it does allow us to define getters. That's what we have to use to port priorities over to ES6:

import { Base } from 'yeoman-generator';

export default class MyGenerator extends Base {

  get prompting() {

    return {

      appName() {

        let done = this.async();
        let prompt = [
          {
            type: 'input',
            name: 'appName',
            message: 'Enter a name for your app:',
          },
        ];

        this.prompt(prompt, ( { appName } ) => {
          this.options.appName = appName;
          done();
        });
      },
    };
  }
}

Yeoman templating

The Yeoman workflows that you're already used to using will not have to change. You have access to all the same methods and they will behave in exactly the same way. One small gotcha to be aware of when you're writing ES6 files as templates is the new template strings feature. Yeoman uses Lodash templating which allows for variable interpolation:

var name = '${ appName } app'; // ES5 with Lodash interpolation
var greeting = 'Welcome to our ' + name;

Let's rewrite the above to take advantange of ES6 template strings:

let name = '${ appName } app'; // Lodash interpolation
let greeting = `Welcome to our ${ name }`; // ES6 template string

This is problematic because Lodash will attempt to perform interpolation both where intended and within ES6 template strings. To work around this we need to tell Yeoman's instance of Lodash to use a different interpolation syntax. The generator's constructor is an ideal place to do so:

export default class MyGenerator extends Base {

  constructor( ...args ) {

    super(...args);

    // Configure Lodash templating so it ignores interpolation markers in
    // ES6 template strings.
    this._.templateSettings.interpolate = /<%=([\s\S]+?)%>/g;
  }
}

This removes the ability to use the ${ ... } syntax for Lodash interpolation. The regular expression we used above forces us to use <%= ... %>:

let name = '<%= appName %> app'; // Lodash interpolation
let greeting = `Welcome to our ${ name }`; // ES6 template string

Compiling and publishing the generator

Having written a Yeoman generator in ES6 you'll need to compile it to ES5 before it can be used. You'll probably also want to publish it to npm. That process is beyond the scope of this article but is detailed in our previous post, 'Using ES6 with npm today'.