By Chris Nwamba
This tutorial is out of date and no longer maintained.
Yeoman is one of the web’s most popular tooling frameworks with over 400 contributed reusable plugins. The open-source project led by Addy Osmani makes project scaffolding an easy step.
This means that basic project structures and dependencies can now be set up with just one or two commands from the CLI. Is that not amazing? You don’t have to install Express
, Passport
, Angular
, Mongoose
, etc. that your MEAN project requires manually, you just need to create and configure once, and then use throughout your other projects.
Note that Yeoman is not just for JavaScript-based projects but for anything you can make out of it.
Yeoman’s plugins are called generators
and are made in such a way that they suit a particular project requirements or workflow. It is not a surprise that out of the 400 contributed generators, you might not find one that suits you.
Today, I will guide you through 4 easy steps to create yours and also provide a demo at the end. We will end up with a MEAN generator for Scotch.
As always, the first step is to install the dependencies and create a new generator project. The concept is actually using a yeoman generator to generate a generator template.
The Yeoman team built a generator plugin that makes building a Yeoman custom generator quite easy. So install both Yeoman (yo
) and the generator (generator-generator
is the name):
- npm install -g yo generator-generator
This command installs both Yeoman and the generator plugin. Installing these tools does not create a project, they just download them to your file system. To set up a generator project, run:
- yo generator
You will be asked two or more questions from the CLI including your GitHub username and your generator base name. Yeoman generators naming convention is in such a way that the generators are prefixed with generator-
and the name of the custom generator (basename) added right after the hyphen (-
).
In our case, I chose, scotchmean
so that at the end, the generator will be used as generator-scotchmean
.
You do not have to be intimidated by the file structure. Actually, we just need one folder which is the generators
folder. The generators
folder has an app
directory with templates
and index.js
. The templates
are files that will be generated when we use this generator to scaffold a project and the index.js folder holds all the logic involved in generating a template.
Before we move to step 2, clear the contents of app/templates/index.js
and replace with:
'use strict';
// Require dependencies
var yeoman = require('yeoman-generator');
var chalk = require('chalk');
var yosay = require('yosay');
module.exports = yeoman.generators.Base.extend({
// Configurations will be loaded here.
});
One awesome feature of Yeoman is its ability to scaffold a project using the user’s decision from inputs. This means that we can configure a lot of options like the project’s name, the dependencies we need, and much more. Yeoman uses Inquire.js to accomplish this feature. To keep it minimal, we just need to get the project’s name from input:
// Configurations will be loaded here.
// Ask for user input
prompting: function() {
var done = this.async();
this.prompt({
type: 'input',
name: 'name',
message: 'Your project name',
// Defaults to the project's folder name if the input is skipped
default: this.appname
}, function(answers) {
this.props = answers
this.log(answers.name);
done();
}.bind(this));
},
// Writing Logic here
We are using the async()
method to make sure that the function does not exit before the actual work gets completed.
The prompt
method is used to get the user’s input which is of type input
as there are other types.
The name
property of the prompt is used to access the field’s value from the project and the message
is the instruction. We also want it to default to the folder name if we do not provide a name while scaffolding.
The second argument of prompt
is a callback that holds the values from the prompt(s). We have set this.props
up with the answers so they can be accessed from the “writing” logic.
Template files are files generated when you use Yeoman with your custom generator to generate a project. These files are located within the templates
folder in the app
directory. It works like common view engines like EJS. You can dynamically fill in data into the templates with the inputs that were generated. We will use a basic MEAN folder structure for our templates:
|---public
|-----css
|-----app.css
|-----js
|-----app.js
|---routes
|-----all.js
|---model
|-----todo.js
|---views
|-----index.ejs
|---server.js
|---bower.json
|---package.json
|---etc.
Just like a normal application, replace the above project structure with the contents of the templates
folder and then we will gradually populate them in a stepwise manner or create them along the process. All template logic is wrapped inside the writing
object in index.js
:
// Writing Logic here
writing: {
// Copy the configuration files
// Copy application files
},
// Install Dependencies
The files to be generated are better classified into configuration files and application files. The config files include package.json
, bower.json
, and other utility files for our project while the app files include server.js
, our routes, public directory, and every other major file.
We will first create the configuration files templates and then copy them. To create the files, open app/templates/_package.json
and replace them with:
{
"name": "<%= name %>",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.13.3",
"ejs": "^2.3.4",
"body-parser": "^1.14.0",
"cookie-parser": "~1.0.1",
"mongoose":"^4.2.4"
}
}
Also, replace app/templates/_bower.json
with:
{
"name": "<%= name %>",
"version": "0.0.0",
"dependencies": {
"bootstrap": "3.3.5",
"angular": "1.4.6"
}
}
Now create a bowerrc
in app/templates/
and fill it with:
{
"directory" : "app/public/libs"
}
Update app/index.js
:
// Writing Logic here
writing: {
// Copy the configuration files
config: function () {
this.fs.copyTpl(
this.templatePath('_package.json'),
this.destinationPath('package.json'), {
name: this.props.name
}
);
this.fs.copyTpl(
this.templatePath('_bower.json'),
this.destinationPath('bower.json'), {
name: this.props.name
}
);
this.fs.copy(
this.templatePath('bowerrc'),
this.destinationPath('.bowerrc')
);
},
// Copy application files
},
// Install Dependencies
Two methods, copy()
and copyTpl()
are used to copy the templates from our template path to a destination path which Yeoman abstracts from us the implementation but helps determine the path we intend to copy the files to.
The difference between the two methods is that copyTpl()
takes a third parameter which is a list of data to be bound to the template file after it is generated while copy()
is used when there are no bindings required in the template.
Now we have the setup files prepared and we can now go ahead and create the application files. I suggest we begin with the server.js
as it is our entry point:
// Dependencies
var express = require('express');
var app = express();
var path = require('path');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var mongoose = require('mongoose');
// Create a mongoose connection
mongoose.connect('mongodb://127.0.0.1:27017/scotchmean');
// Load custom dependencies
var routes = require('./routes/all');
// View engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// Configure Body Pareser and Cookie Parser
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
app.use(cookieParser());
// Use the public folder for static files
app.use(express.static(path.join(__dirname, 'public')));
console.log(routes.getTodo)
// Create the routes
app.get('/', routes.index);
app.get('/todo', routes.getTodo);
app.post('/todo', routes.postTodo);
// Set port to env.Port or default to 8080
app.set('port', process.env.PORT || 8080);
// Listen to port for connections
app.listen(app.get('port'), function() {
console.log('App listening at port ' + app.get('port'));
});
The above snippet is a basic server file for Node. We just ended up loading routes (though yet to be implemented), configuring body and cookie parser, creating a connection to MongoDB, and listening to a port.
Next up is to create the routes which include: index
, getTodo
and postTodo
. Create a _routes
folder inside the templates
directory. Add _all.js
with the following content:
var Todo = require('../model/todo').todo;
exports.index = function(req, res){
res.render('index');
};
exports.getTodo = function(req, res){
Todo.find().exec(function(err, todo){
if (err)
return res.send(err);
return res.json(todo);
});
};
exports.postTodo = function(req, res){
Todo.create(req.body, function(err, todo){
if (err)
return res.send(err);
return res.json(todo);
});
};
We can now create the Todo model that the route loaded. Create a _model
folder in templates
directory and add a file named _todo.js
in it with the following content:
var mongoose = require('mongoose');
var todo = mongoose.model('Todo', {
content: String
});
exports.todo = todo;
Time to develop our views and public files. We need just one EJS view which is index
for our home page as seen in the routes. This file will live in the _views
folder in the templates
directory:
<html ng-app="app">
<body ng-controller="TodoController">
<div class="container">
<div class="col-md-4 col-md-offset-4">
<input type="text" ng-model="todo.content" class="form-control">
<button type="button" ng-click="addTodo()" class="btn btn-primary">Add</button>
<hr>
<ul>
<li ng-repeat="todo in todos">{{todo.content}}</li>
</ul>
</div>
</div>
<script src="libs/angular/angular.js"></script>
<script src="js/app.js"></script>
</body>
</html>
See how we are binding the name
value to the view in the title
. That is the most interesting part of this file. The rest is just basic HTML with Bootstrap and Angular codes.
Moving further, create the _public
folder with _css
and _js
directories. In the _js
folder add a file _app.js
to hold our Angular logic:
angular.module('app', [])
.controller('TodoController', function($scope, $http) {
$scope.todos = [];
$scope.todo = {};
$http.get('/todo').success(function(data) {
$scope.todos = data;
});
$scope.addTodo = function() {
$http.post('/todo', $scope.todo).success(function(data) {
$scope.todos.unshift(data);
});
$scope.todo = {};
};
});
Now create a file in the _css
directory named _app.css
with this content:
body {
padding-top:50px;
}
We have created all the template files that our project needs but have yet to update the index.js
to be able to scaffold and serve them at project scaffold. Let us do so:
// Writing Logic here
writing: {
// ...
// Copy application files
app: function() {
// Server file
this.fs.copyTpl(
this.templatePath('_server.js'),
this.destinationPath('server.js'),
this.destinationPath('/views/index.ejs'), {
name: this.props.name
}
);
// Routes
this.fs.copy(
this.templatePath('_routes/_all.js'),
this.destinationPath('routes/all.js')
);
// Model
this.fs.copy(
this.templatePath('_model/_todo.js'),
this.destinationPath('model/todo.js')
);
// Views
this.fs.copyTpl(
this.templatePath('_views/_index.ejs'),
this.destinationPath('/views/index.ejs'), {
name: this.props.name
}
);
// Public/
this.fs.copy(
this.templatePath('_public/_css/_app.css'),
this.destinationPath('public/css/app.css')
);
this.fs.copy(
this.templatePath('_public/_js/_app.js'),
this.destinationPath('public/js/app.js')
);
}
},
// Install Dependencies
Finally, install all the dependencies once the scaffold is completed by updating index.js
to:
// Install Dependencies
install: function() {
this.installDependencies();
}
The most reasonable way to carry out an integration test is not to publish to npm registry and then install before we can run yo scotchmean
to create a project.
We can utilize npm’s link
to assist us with linking the generator in the global node modules. To understand better, on the generator’s project folder, run:
- npm link
Navigate to a folder you would love to scaffold a new MEAN project and run:
- yo scotchmean
Respond to the prompts and watch yo
do its magic. Do have in mind that you do not need to keep running npm link
command to update changes before testing. Just edit and save then your project will be ready for another scaffold.
When you are satisfied with what you have built, you can publish to npm.
Hopefully, you can start making your life easier with Yeoman and its generators. It doesn’t end with a basic example; publish on npm, share on GitHub, and earn some credits.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.