Understanding Yeoman
January 20, 2016
What is Yeoman?
(45 mins) Yeoman is a simple, structured, and powerful code generation tool used to scaffold code of any kind or amount.
A rich ecosystem of open-source, extensible Yeoman generators, built by the community, is available on the Yeoman website. Find a generator you’re interested in and bootstrap an application quickly, or read on to learn how to build one yourself.
How do I use it?
Prerequisites: Ensure npm1, node, and yeoman2 are installed globally
First, what is a generator? Well a generator, as simply as possible, is a script that runs with the intention of creating source code files. It can generate one file or an entire project. By providing boilerplate code, we can harness learned best practices that serve as a solid foundation for our application.
But Yeoman isn’t just about the beginning of the project. It can be used during the entire product development lifecycle. Since Yeoman uses Node, we can harness the full power of the web to generate our code from information gathered from any connected source, including other web servers and services.
However, before we can run, we must walk. Lets get those keys clacking and run our first generator.
Run a generator
There are thousands of different yeoman generators. Once you find one you’re interested in, getting a complete application up and running is as easy as 1 2 3.
- Find a generator from the Yeoman website and install it with npm (i.e.
npm install generator-angular -g
) 3 - Run the generator (i.e.
yo angular
) - Follow the prompts…
Sub-generators are just as easy and passing in arguments are a breeze. Simply enter something similar to yo angular:controller MainController
to scaffold a new Angular controller named MainController. You can see here how easy it is to pass arguments to the generator.
Create a generator
Yeoman generators are configuration based and creating a Yeoman generator is quite simple. There are just a few idiosyncratic choices that you need to be aware of, but once you understand a few key concepts, you can create you own code templates and merge them with users’ answers to create code files quickly and easily. After that, we’ll learn how to persist config values and take advantage of Yeomans virtual file system5 so we can rerun our generators without fear. Then we’ll get to creating sub-generators and finally how to test our generators with Mocha.
The Generator
Start by creating a directory for your project. We’ll call it “HelloYeoman”. Change into your new directory and run npm init
(a Yeoman script is simply a node script after all) to create your package.json. The package.json file defines the name of your generator (remember it’s prepended with generator-
. Make sure you have a “main” property to define the generator entry point (usually index.js).
Next, run npm install yeoman-generator --save
to bring in the yeoman-generator library. This is the library that makes your script a Yeoman generator.
By convention, Yeoman uses the ./app
directory to contain the base generator. Create your script at ./app/index.js
. Whenever you run the generator, it will start with this file.
Following is a “Hello World” Yeoman script.
'use strict';
var generator = require('yeoman-generator');
var yeomanConfig = {
helloWorld: function(){
this.log('Hello World');
}
};
module.exports = generator.Base.extend(yeomanConfig);
Notice the use of the common.js (Node) Require syntax.
To enable running of your generator we have to make sure Node can find it by running npm link
in the base directory of the generator. This “link” command creates a symbolic link for this directory in Node’s path. We can now run our generator yo HelloYeoman
and you should get “Hello World” in your console. Easy peasy, right?
So all of that code up above should look pretty familiar. The only bit that’s not standard JS is Base.extend
. The first part of that “Base” determines what kind of generator we want to use. The second part allows us to “extend” that “Base” generator. Yeoman has two basic kinds of generators: Base
and NamedBase
. The only difference is that the NamedBase adds the “name” argument and makes it required. By convention, the NamedBase base is typically used for sub-generators, where we typically create a named class or file.
Config Conventions
The most fundamental thing to know about Yeoman is the order of execution. Every function defined in your Yeoman config will run in top-to-bottom order, however any method prepended with _
will be considered “private”, and some methods are special. The constructor
method will run first and serves as the constructor function for your Yeoman script. Next to understand is the “Yeoman Running Context” or “run loop” (even though it’s not a loop since it only gets executed once). There are eight special functions that have a well defined execution order: initializing
, prompting
, configuring
, default
, writing
, conflicts
, install
, and end
. The names are fairly descriptive and you should understand what each of these 8 steps is for by the end of this post, if you don’t already. Any method not named one of these names will be executed after “default” (so the 4.5 slot). As a best practice, you should put these steps in this order in your script file. Each step is a function, or an object with arbitrarily named function members that run sequentially.
Code templates
The most useful part of Yeoman are the code templates. They live in the ./app/templates
directory and they are your code, intermingled with variables merged from the answers provided by the user.
Execution Contexts
There are two contexts for Yeoman, the “Template Context”. This is where the templates are stored. Use this.templatePath()
to get/set the path. A common convention is to prepend each filename with _
for a regular file and remove the dot from dot-files like .gitignore
.
The “Destination Context” is where your application will be scaffolded and can be retrieved with this.destinationPath()
.
Copying Files
To copy a file use this.copy(templatePath, destinationPath);
.
To copy a directory use this.directory(templatePath, destinationPath);
These are relative paths to the file/directory you want to copy based on the above execution contexts.
Templating Syntax
Use <%= variableName %>
in your code to create a merge variable
Since we’re merging values with our template, we don’t want to straight copy it, so we have to use a different command:
this.fs.copyTpl({
this.templatePath('index.html'),
this.destinatonPath('src/index.html')
{
variableName: 'Foo'
}
})
Create JSON Files Programmatically
Create your JS object literal, then use this.fs.writeJSON(fileName, objectLiteral);
Run npm install && bower install
automatically.
Inside the “install” step enter npmInstall(); installBower();
or, simply installDependencies();
Since this can be an expensive task, to skip this step use the --skip-install
option. Use it like an option, it’s not automatic.
Arguments
Arguments allow us to pass in variables to our generator. These arguments manifest as objects on the local scope. Arguments are typically strings and used for naming7 the file or object.
this.argument('name', {
type: String,
required: true
});
Use it like foo = this.name
.
Options (command line switch)
Options are very similar to arguments, but are typically booleans
this.option('includeUtils', {
desc: 'An optional description',
type: Boolean,
default: false
})
Use it like this.options.includeUtils ? foo: bar;
.
Prompts
Prompts allow us to ask the user questions so we can customize the code that gets generated. Behind the scenes, Yeoman uses inquirer.js as the prompting interface. Checkout their github page for documentation of the prompt syntax
The following code is a basic prompt. The code belongs inside the prompting
function.
var done = this.async();
this.prompt({
type: 'input',
name: 'appName',
message: 'What is the application name?',
default: 'app'
}, function callback(answers){
this.log(answers);
done();
}.bind(this));
Persist the Config
We can store our prompt responses with the Yeoman storage API.
Simply put the store: true
property on the prompt config and Yeoman will remember the last answers provided to the prompts.
To get direct access to the Storage API inside the generator use this.config.set('appName', 'helloWorld');
to set a config variable and this.config.get('appName');
to get the config variable.
Generator composition8
Generator composition is a great feature of Yeoman. We can use another generator as the basis for ours or we can require one as a dependency and include it somewhere else.
We can take generators already created by someone else and use them as a component of our generator. It’s as simple as using this.composeWith(namespace, options, settings)
inside any function to make that generator run at that step. Namespace is the generator name, options are the just that generators options (refer to above options section).
Sub-generators
Rhetorically, there are two types of Yeoman generators, a base generator and sub-generators. However, they’re really the same thing. The difference is that a base generator creates an entire application, whereas sub-generators create application components. A sub-generator is the same as any base generator. The only difference between the two are that the base generator lives in the top-level app
directory whereas sub-generators live in top-level folders named anything else. Run the sub-generator by appending :[name]
and any options to the base generator (i.e. yo angular:controller foo
).
How to Test a Yeoman generator
I like Mocha, so I will use it’s BDD style syntax to create my tests.
First, run npm install mocha --save-dev
to ensure Mocha is installed in your project as a dev dependency.
Next, create a directory named “test” and a file inside it called “test-app.js” with the following code…
'use strict';
var path = require('path');
var assert = require('yeoman-generator').assert;
var helpers = require('yeoman-generator').test;
describe('app generator', function(){
describe('default', function(){
before(function(){
helpers.run(path.join(__dirname, '../app'))
.withArguments(['HelloYeoman'])
.withOptions({ skipInstall: true })
.on('end', done);
});
// Check creation of files
it('creates files', function(){
assert.file([
'package.json',
'src/app/app.js'
])
});
// Check File Contents
it('sets file content', function(){
assert.fileContent('src/app/app.js', /angular.module\('app'/);
});
});
// Respond to Prompts
describe('npapp prompt', function(){
before(function(){
helpers.run(path.join(__dirname, '../app'))
.withArguments(['HelloYeoman'])
.withOptions({ skipInstall: true })
.withPrompts({ ngappname: 'fooApp' })
.on('end', done);
});
it('injects custom app name', function(){
assert.fileContent('src/app/app.js', /angular.module\('fooApp'/);
});
});
});
Notice the ‘HelloYeoman’ generator name in the arguments, and the regular expression in the fileContent check.
We can then run mocha
on the command line to execute our tests.1
Appendix
-
To use npm and node effectively, we need to ensure that we can run the commands without sudo. To do that, follow this guide from John Papa. For more info, you can read Stack Overflow or Sindresorhus or Isaacs guide
-
Yeoman generators are simply node apps with
generator-
prepended to the app (generator) name. This means that whenever you find a yeoman generator you’d like to install and use, simple prepend generator- to the name of the generator and install it with npm as you would with any other npm package. The generator will then live in your node package folder for later reuse. -
yodoctor
will provide health information about your Yeoman installation. It is run automatically whenever Yeoman is installed. -
Yeoman is intelligent about the way it generates/copies/writes your files in a synchronous manner. It uses an in-memory file system that allows intelligent diffing of files, so the user will be aware of conflicts and can decide whether to overwrite a file that already exists. When a generator is run and files are copied the file status is shown next to the file path in the console.
-
Prettify your generator…
npm install yosay && chalk --save
<br>log(yosay(chalk.yellow('Welcome to the Hello World Generator')))
-
Use lodash to morph the your arguments to kebab-case, camel case, or title case.
-
Generator-Common is a good candidate for generator composition. In that case, the settings object should look something like…
{ local: require.resolve('generator-common'); }
Credits
This post was heavily inspired by Steve Michelotti’s Yeoman course on Pluralsight. I recommend you watch Steve’s course if anything in this post was confusing.