Authoring Yeoman Generators

The last couple of days I've been playing around with authoring a Yeoman generator for scaffolding out a Sketch app plugin.  While it's not completely done yet, it's in a "good enough/just ship it" state to put the source on github.  I'll be doing some posts in the future on how to create your own Sketch plugins, for which this generator will come in handy, but the purpose of this post is to go over some of the hurdles I faced and some "not-easily-found" documentation for those who are in the process of building their first Yeoman generators.  The existing documentation is pretty helpful but as with any software project, you sometimes need to know where to look to find the information you need.

Some things to be mindful of:

package.json

The first thing you'll likely do when creating your generator is add your package.json file.  Most generators are structured like so:

├───package.json ├───app/ └───index.js

and if you have sub-generators, you're structure might look like this:

├───package.json └───generators/ ├───app/ └───index.js └───router/ └───index.js

Yeoman will look in ./ and generators/ for available generators.  If you've got sub-generators, the key is to add them to your package.json file, like so:

{
    "files": [
        "generators/app",
        "generators/router"
    ]
}

Yeoman uses the Grouped-queue project to group tasks into a priority queue.  The priorities are as follows

  1. initializing - Your initialization methods (checking current project state, getting configs, etc)
  2. prompting - Where you prompt users for options (where you'd call this.prompt())
  3. configuring - Saving configurations and configure the project (creating .editorconfig files and other metadata files)
  4. default - If the method name doesn't match a priority, it will be pushed to this group.
  5. writing - Where you write the generator specific files (routes, controllers, etc)
  6. conflicts - Where conflicts are handled (used internally)
  7. install - Where installation are run (npm, bower)
  8. end - Called last, cleanup, say good bye, etc

This is something that is important to be aware of.  It's in the official docs, but easy to skip over.

If you want to put tasks in the default task (#4 above), you can code them like so

priorityName: {
    method: function () {},
    method2: function () {}
}

Question object

Another piece of the docs that's easy to miss - when you're coding the prompting function, the available prompt properties are

  • type: (String) Type of the prompt. Defaults: input - Possible values: input, confirm, list, rawlist, password
  • name: (String) The name to use when storing the answer in the answers hash.
  • message: (String|Function) The question to print. If defined as a function, the first parameter will be the current inquirer session answers.
  • default: (String|Number|Array|Function) Default value(s) to use if nothing is entered, or a function that returns the default value(s). If defined as a function, the first parameter will be the current inquirer session answers.
  • choices: (Array|Function) Choices array or a function returning a choices array. If defined as a function, the first parameter will be the current inquirer session answers. Array values can be simple strings, or objects containing aname (to display in list), a value (to save in the answers hash) and a short (to display after selection) properties. The choices array can also contain a Separator.
  • validate: (Function) Receive the user input and should return true if the value is valid, and an error message (String) otherwise. If false is returned, a default error message is provided.
  • filter: (Function) Receive the user input and return the filtered value to be used inside the program. The value returned will be added to the Answers hash.
  • when: (Function, Boolean) Receive the current user answers hash and should return true or false depending on whether or not this question should be asked. The value can also be a simple boolean.

This question object is from Inquirer.js, another project from the Grouped queue author.

Running plugin locally

In order to test out your plugin, from the root of your generator project run npm link and this will symlink your generator folder so that you can run yo <your plugin name> without having to export the project as an npm module or install it as a module.

I would recommend installing the yeoman-generator package globally, because even though this should be a dependency in your package.json, when I ran the symlinked project it had issues finding the package.

A word on cloned git repos

If you're developing and debugging your generator from a git clone - as I was - you might run into issues with the generator behaving oddly.  In particular, running my code from this git clone caused issues with the 'writing' function.  This function would get skipped over and I was not able to figure out why.  Maybe the problem is obvious to others, but if you face similar issues, I would recommend copying to a fresh folder and doing your development from there.

Why author a generator?

If you're only vaguely familiar with this technology you might wonder what benefits it provides.  I would recommend using generators for two reasons:

  1. By quickly scaffolding an application, you're able to save a lot of potential headaches and spend a lot more time actually building your application or tool
  2. If you work in a large, enterprise type environment, there are likely multiple teams working on similar applications and technology stacks.  Utilizing a generator can help ensure you're following the same patterns for structuring applications across teams.

That's it for now.  There are plenty of tutorials out there that will walk you through building a generator, but hopefully this post will help you navigate past some of the gotchas I encountered.

*As a note to myself, some features I'd like to add to the Sketch generator in the future are:

  • Prompt validations
  • Rewrite in ES6