Writing a generator for Yeoman.io

image
Good day, habrasociety! In this article I want to describe the experience of creating a generator for the scaffolding system Yeoman . First of all, I was a little surprised that this system and work with it were not described on the hub, except for one small mention from the distant 2012: Yeoman.io . As I wrote above, in this article I will consider the phased creation of a yeoman generator for your projects.

The yeoman generator (hereinafter simply referred to as the generator) is an npm package with the help of the directives of which yeoman builds the application framework. In this article, I will consider an example of creating a generator for scaffolding the architecture that I use on my projects (marionette, coffee, sass + compass, require).

Initial data

We need: a machine with installed nodejs , npm , yeoman and the npm package generator-generator .
Next, we need to create a directory in which our generator will be located (I named my generator-puppeteer ). It is very important that your folder starts with the generator- prefix , as otherwise, at the start of work, yeoman will create a folder that is modeled on the principle generator- <generator name>.

Step 1 - Scaffolding the Generator



# Создадим директорию для нашего генератора
$ mkdir generator-puppeteer && cd $_
# Развернем каркас нашего генератора
$ yo generator


After two questions about the username on github and the name of the generator, yeoman deploys the skeleton of our future generator.
Let's see what yeoman generated for us:

Directories:

app - the directory where all our files related to the project content will be located, for example: bower.json, package.json, templates for all our pages, etc.
node_modules - a directory with generator dependencies dictated by package.json, for example chalk or mocha .
test - here all tests for our generator will lie.


Files:

.editorconfig - config for a text editor
.gitattributes - specific settings for directories or files for git
.gitignore - list of files and directories that will not be indexed git
.jshintrc - config jshint
package.json - generator dependencies file
README.md - description file project for github
.travis.yml - specifying a platform for CI


So, the skeleton of our generator is deployed.

Step 2 - Editing the Run File


Personally, when I see an unfamiliar architecture, a logical question arises: where is the entry point to the project. In our case, this is the index.js file located in the app directory. It works as follows: first, we get access to the package.json file and subscribe to the initialization completion event. If the --skip-install flag was not passed, then after initialization, the dependencies specified in package.json and bower.json will be installed. Nothing complicated, right? Now let's try to customize the standard UI of the scaffolder. To do this, we will have to change the askFor method - it is he who is called the first after initialization and is responsible for polling the necessary information from the user (and also draws pretty nice ASCII art). This method uses the Inquirer library implementation, allowing you to create questions and receive information from the user. Let's try to find out something interesting from the user, for example, the name of his application:

Source code:

var prompts = [{
  type: 'confirm',
  name: 'someOption',
  message: 'Would you like to enable this option?',
  default: true
}];
this.prompt(prompts, function (props) {
  this.someOption = props.someOption;
  done();
}.bind(this));

Edited Code:

var prompts = [{
  type: 'prompt',
  name: 'appName',
  message: 'Could you tell me the name of your new project?',
}];
this.prompt(prompts, function (answers) {
  this.appName = answers.appName;
  done();
}.bind(this));


You can find more information in their repository, on the examples page . Using this library here will be most useful if you decide to give the user the opportunity to select additional technologies that he might want to include in the project, for example, you can ask the user if he wants to include the ability to use bootstrap “out of the box” in the project. As you noticed, all variables are written as properties of the generator instance - later, we will use them inside the templates.

Step 3 - Writing Directives to Scaffold the Application Structure


Now let's look at the app function, the heart of our generator. This is where we assemble the framework of our application. What happens in the body of this function:

app: function () {
  this.mkdir('app');
  this.mkdir('app/templates');
  this.copy('_package.json', 'package.json');
  this.copy('_bower.json', 'bower.json');
}

As we can see, by default everything is very fresh here: we just create 2 directories and copy 2 templates to our project directory. The copy function takes only two parameters: the source file from sourceRoot and the name of the file to be created in targetRoot. Let's write the code that will create the index.html file for us. But probably I want to change the contents of the index depending on the options that I can choose before installation. For example, I want to set the name of my project in a tag - this.copy is not enough here, this.template will help us here. Let's dwell on these functions in a bit more detail. Both functions are part of the actions / actions impurity , and are performed to move files from the template directory to the application directory, with one exception: the template functionable to work with templates, i.e. with it, we can copy the file from sourceRoot, paste the data into it and send it to targetRoot. Let's try to do this using the example described above. Create the _index.html file in the sourceRoot of the project directory (app / templates by default). As an example, you can use this gist . Now let's add a little app function to get something like this:

app: function () {
  this.mkdir('app');
  this.mkdir('app/templates');
  this.template('_index.html', 'index.html');
  this.copy('_package.json', 'package.json');
  this.copy('_bower.json', 'bower.json');
}

So where do we get the data for our template? By default, if data is not passed explicitly by the third attribute, then the template engine uses the scope of the generator as a hash with the data, i.e. when we saved the appName entered via prompt in this.appName, we automatically made it available in all our templates (where the data hash is not directly specified). Great, now we can parameterize our files. The next step is architecture design. Since I am writing a generator for the architecture of my project, then in this article I will rely on its architecture, namely:

app - application root
app / templates -
app / core templates - base classes
app / common - different impurities, etc.
app / static - statics (images, fonts)
app / components - components
app / modules - modules
app / stylesheets - styles
app / libs - third-party libraries

This completes the architectural component, it remains only to configure the libraries that we want to use by default. But the question is: we are writing a generator that will be used by different people who share our view on the architectural solutions of the application, but will they all use the same tool-chain? Unlikely. We, as decent developers, of course, should foresee such a moment and add at least the minimum selection of technologies that our generator out of the box plans to support. In my case, it will be RequireJS, CoffeeScript and SASS + Compass, and each time I use my generator, the user will be asked which of the technologies he wants to add to the project. And don't forget to add the Gruntfile! Given these additions, the code for our app method will be as follows:

app: function () {
  // Core application folder
  this.mkdir('app');
  // Templates application folder
  this.mkdir('app/templates');
  // Folder for base classes
  this.mkdir('app/core');
  // Common project files
  this.mkdir('app/common');
  // Static content, like images or fonts
  this.mkdir('app/static');
  // Logic components for the project
  this.mkdir('app/components');
  // Modules of the project
  this.mkdir('app/modules');
  // Stylesheets directory
  this.mkdir('app/stylesheets');
  // 3-rd party members libs
  this.mkdir('app/libs'); 
  this.template('Gruntfile.js', 'Gruntfile.js');
  this.template('_index.html', 'index.html');
  // RequireJS config & App
  this.copy('js/app.js', 'app/app.js');
  this.copy('js/main.js', 'app/main.js');
  this.copy('_package.json', 'package.json');
  this.copy('_bower.json', 'bower.json');
  this.copy('_.bowerrc', '.bowerrc');
}


Please note, I add the .bowerrc file at the end, in it I indicate that the dependencies should be stored in the app / libs directory.

Step 4 - Creating a Sub Generator


So, albeit not very deep, we were able to write a simple generator for the project structure and index.html, which will be the entry point to our project. It seems to be nice, right? But Yeoman can do more! Let's try to squeeze a little more out of it!
Based on what we have already written, yeoman is used only for the deployment of architecture at the initial stage, but now we will use it to create templates for the components of our application. As I wrote above, in our project (at least in my architecture of our project), I added the app / components folder, in which I am going to add some abstract components; Now a little more detailed: by a component, I mean a certain organization of code like MVC, which makes it easier to work with logical entities. So, for example, a block with comments should be on several pages of our application. In order not to copy-paste the code from the module and keep it always in a consistent state, we create a CommentComponent, which we call from different modules through its API, for example:

var _this = this;
var commentComponent = new CommentComponent;
commentComponent.getUserComments({user_id: 1}).done(function(commentsView) {
  _this.layout.comments.show(commentsView);
});

Accordingly, it would not hurt me if I could create such components as quickly as possible (after all, no one likes to create a bunch of files and folders?). As you say, if our component is created by a convenient team

# Создаем компонент CommentsComponent
$ yo puppeteer:component 'Comments'

So, let's decide what this team should be able to? For example, create an MVC architecture in the app / components / comments folder, as well as generate the required minimum set of files:
models / comment.js
collections / comments.js
views / comments.js
views / comment.js
controller.js


Let's see what we need to do. To start, let's create our sub-generator frame. To do this, run the following command from the root folder of our generator:

# Создаем саб-генератор "component"
$ yo generator:subgenerator 'component'

So, let's see what he generated for us:
app / component
app / component / index.js
app / component / templates / somefile.js

At its core, a sub-generator is the same ordinary generator, has the same API and almost the same structure as its older brother. So, what do we see when we open index.js: our component inherits from NamedBase and has 2 predefined methods: init and files. As you might guess, in init we just get greetings-msg for the calling sub-generator, and in the files method we describe directly all the logic of the generator. I will not focus on this because there is nothing new here. You can see my example index.js in my gist .

Next, create the files themselves templates. Also nothing new, we have already done this above. As usual, you can find my version here .

Step 5 - Launching Our Generator


To start our generator, we first need to create a link to our npm package. To do this, from the generator folder, you need to run the command:

$ npm link

Now that the link is created, we can create a test directory and feel what we got:

$ mkdir TestProject && cd $_ && yo puppeteer

Also popular now: