Good day, habrasoobschestvo! In this article I want to describe the experience of creating a generator for scaffolding
Yeoman system. First of all, I was a little surprised that this system and work with it were not described in Habré, 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.
Yeoman-generator (hereinafter simply a generator) is an npm-package, with the help of directives which yeoman collects the application framework. In this article I will look at an example of creating a generator for scaffolding the architecture that I use on my projects (marionette, coffee, sass + compass, require).
Initial data
We will need: a machine with
nodejs ,
npm ,
yeoman and the
generator-generator npm package installed.
Next, we need to create a directory in which our generator will be located (I called my
generator-puppeteer ).
It is very important that your folder starts with the prefix generator- , since otherwise, at the beginning of work, yeoman will create a folder that is configured on the principle of generator- <generator name>.
Step 1 - Generator Scaffolding
')
After two questions about the username on github and the name of the generator, yeoman unfolds the skeleton of our future generator.
Let's see what yeoman generated for us:
Directories:
app - directory where all our files will be located, related to the project content, for example: bower.json, package.json, templates for all of our pages, etc.
node_modules is a directory with generator dependencies dictated by package.json, such as chalk or mocha .
test - all tests for our generator will lie here.
Files:
.editorconfig - config for text editor
.gitattributes - specific directories or file settings for git
.gitignore - a list of files and directories that will not be indexed by git
.jshintrc - jshint config
package.json - generator dependency file
README.md - project description file for github
.travis.yml - platform indication for CI
So, the skeleton of our generator is deployed.
Step 2 - Editing the Run File
Personally, when I see unfamiliar architecture, a natural 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 like this: first we access the package.json file and subscribe to the end of initialization event. If the --skip-install flag is 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 scoutholder UI. To do this, we will have to change the askFor method - it is he who calls the first after initialization and is responsible for polling the necessary information with the user (and also draws a rather nice ASCII art). This method uses the implementation of the
Inquirer library, which allows you to create questions and receive information from the user. Let's try to find out from the user something interesting, for example the name of his application:
Source:
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));
More information can be found
in their repositories, on the examples page . Using this library here will be as useful as possible if you decide to give the user the choice of additional technologies that he may want to include in the project, for example, you can ask the user if he wants the project to include the ability to use bootstrap out of the box. As you noticed, all variables are recorded as properties of the generator instance - later, we will use them inside the templates.
Step 3 - Writing Directives for Scaffolding the Application Structure
Now let's look at the app function - the heart of our generator. This is where we build the framework for 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 pretty here: we just create 2 directories and copy 2 templates into the directory of our project. The copy function takes only two parameters: the source file from sourceRoot and the name of the file that will be created in the 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 installing. For example, I want to set the name of my project in the tag - here this.copy can’t do without, this.template will help us here. Let's look at these features in a bit more detail. Both functions are part of the
actions / actions mix , and are performed to move files from the template directory to the application directory, with one exception:
the template function can work with templates, i.e. with its help we will be able to copy the file from sourceRoot, insert the data into it and send to the targetRoot. Let's try to do this with the example described above. Create a _index.html file in the sourceRoot of the project directory (by default, app / templates). As an example, you can use this
gist . Now let's finish the app function a little bit 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 the data is not passed explicitly by the third attribute, the template engine uses the scope of the generator as a hash with the data, i.e. when we saved the appName entered via the prompt in this.appName, we automatically made it available in all of 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, in this article I will rely on its architecture, namely:
app - the root of the application
app / templates - templates
app / core - base classes
app / common - different impurities, etc.
app / static - static (images, fonts)
app / components - components
app / modules - modules
app / stylesheets - styles
app / libs - third-party libraries
This completes the architectural component; all that remains is to set up 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 of the architectural solutions of the application, but will they all use the same tool chain? Hardly. We, as decent developers, of course, should foresee such a moment and add at least the minimum choice of technologies that we plan to support out of the box with our generator. 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 do not forget to add Gruntfile! Given these additions, the code for our app method will be as follows:
app: function () {
Please note, I add a .bowerrc file at the end, in which I specify that the dependencies should be stored in the app / libs directory.
Step 4 - Creating a Sub Generator
So, let it go and not really go deep, we could write a simple generator for the project structure and index.html, which will be the entry point to our project. It seems to be good, right? But Yeoman can more! Let's try to squeeze out of it a little more!
Based on what we have already written, yeoman is used here only for deploying the 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, into which I am going to put some abstract components; Now a little more detail: by component, I mean some kind of code organization like MVC, which allows you to simplify working with logical entities. For example, on several pages of our application there should be a block with comments. In order not to copy 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 (because no one likes to create a bunch of files and folders?). As you say, if our component will be created by a convenient command
So let's define what a given team should be able to do? 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 begin with, let's create our sub-generator framework. To do this, from the root folder of our generator, run the following command:
So, let's see what he generated to us:
app / component
app / component / index.js
app / component / templates / somefile.js
At its core, the sub-generator is the same ordinary generator, has the same API and almost the same structure as its older brother. So, what we see when we open index.js: our component is inherited from NamedBase and has 2 preset methods: init and files. As it is easy to guess, in init we simply output greetings-msg for the caller of the sub-generator, and in the files method we describe, directly, all the logic of the generator. I will not focus on this, because nothing new here. My example index.js you can see
in my gist'e .
Next, create the templates for the files themselves. Also nothing new, we have already done it above. As usual, you can find my version
here .
Step 5 - Run 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 has been created, we can create a test directory and feel what we did:
$ mkdir TestProject && cd $_ && yo puppeteer