Improving developer efficiency with generators
A common mantra in programming circles is to keep your code DRY, which stands for Don't Repeat Yourself. Usually we mean this in the context of a codebase — write reusable modules, componetise UI, generalise patterns — but it also applies across projects, especially for freelancers and agencies. If you are starting every website project you build from scratch, then you can drastically improve development productivity by taking advantage of generators.
Boilerplate is the problem
Every project has foundational code and doesn't contribute uniquely to that project. This is boilerplate, code that can be used across projects without much alteration. And in modern frontend development, boilerplate can make up a lot of a codebase. We've touched on how to cut down on React build boilerplate using Neutrino before, but even without configuring Webpack and friends by hand, you have framework boilerplate, environment boilerplate, common components, style libraries, even folder structure and reusable assets that are repeated in almost every project.
Rewriting all of this for every project (or sloppily copy-and-pasting) is not only time consuming, it lends itself to error and encourages inconsistency between projects. Even rewriting the boilerplate of modules like React components (function definitions, Storybook stories, etc) within a single project is inefficient and error prone.
Generators are the answer
It's at this point that people may point to the dozens of community boilerplates that exist — like Create React App, React-Boilerplate, and Bootstrap — as a solution. And 3rd party boilerplates can be a wonderful way to quickly put together an MVP or prototype. Many are high quality and written by very talented people. But they are not a good long-term solution for the professional agency or developer who is building project after project with similar foundations.
Prepackaged boilerplates are designed to get a project up and running as fast as possible. They are loaded full of things a finished product doesn't need and rarely fit a project perfectly, which means you end up fighting against the thing that was meant to save so much time. You could create your own tailored boilerplate, perhaps from a past project. But this too has problems: it has to be tweaked and setup anew for every project, and doesn't account for repeated code within a project.
Instead, create and use generators. A generator is like an interactive boilerplate, with an interface for scaffolding new projects and customising your predefined foundations on the fly. You can build generators both for whole projects and for individual modules within a project. They speed up development without bloat, and encourage consistency from beginning to finished product.
Scaffolding projects with Yeoman
We recommend using Yeoman to create generators for projects. It's the most established generator framework, and has a large community and great documentation to fall back on.
While you can use one of the thousands of prebuilt community generators, we encourage you to create your own. It's easy to do by taking the boilerplate from of a project you've built in the past, and layering Yeoman's scaffolding and templating logic on top of it.
Setup Yeoman
Start by creating the following basic folder structure:
├─package.json
└─generators/
├───app/
├───index.js
└───templates/
The index.js
file in the app/
folder contains all the logic for your generator, and the templates/
folder contains your custom boilerplate.
Install yeoman-generator
from NPM, and then extend the base class it provides in index.js
.
npm install yeoman-generator
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
// Generator logic here
};
Define prompts
Prompts allow you to customise your boilerplate on the fly when scaffolding a project, by asking questions and using the answers in Yeoman's templating engine.
Call this.prompt(prompts)
in the generator to create prompts, with configuration passed to Inquirer.js (see its API documentation on questions and answers). Since prompting is asynchronous, wrap it in an async
function and await
the answers, assigning them to a this.answers
property you can use later in scaffolding.
module.exports = class extends Generator {
async prompting() {
this.answers = await this.prompt([
{
type: 'input',
name: 'name',
message: 'Project name',
},
{
type: 'confirm',
name: 'some-feature',
message: 'Enable <some feature>?',
},
]);
}
};
Scaffold templates
Yeoman uses the EJS templating engine for its scaffolding tools. Use it anywhere you want to customise your boilerplate based on the prompts we setup earlier
<html>
<head>
<title><%= props.title %></title>
</head>
</html>
Then call Yeoman's this.fs.copyTpl(src, dest, props, templateOpts, copyOpts)
function in a writing()
method, passing the answers you gathered before to its template props. You can use Yeoman's this.templatePath
and this.destinationPath
filesystem helpers for getting paths.
class extends Generator {
writing() {
this.fs.copyTpl(
this.templatePath('**/*'),
this.destinationRoot(),
{
props: this.answers
},
{},
{
// Include dotfiles
globOptions: {
dot: true
}
}
);
}
}
Run the generator
Either publish your generator to NPM under the name pattern generator-\[name\]
and install it globally, or link it locally using npm link
. Install the yo
runner, and run your generator in a fresh folder.
npm install yo [generator-name] -g
yo [name]
That's it! Follow your prompts and scaffold out a custom, tailored boilerplate specific to your project. For much more information, see Yeoman's documentation.
Scaffolding internal components with Plop
Plop has a similar ethos and structure to Yeoman, except that it's designed to be used within a project. Whereas Yeoman speeds up the scaffolding of a project, Plop allows you to generate new modules once your project is already setup. It's a fantastic tool for encouraging consistency and improving efficiency while in development. While Plop can be used to generate anything, we're going to look at using it to create new React components.
Setup Plop
Start by installing Plop both as a development dependency in your project and globally on your system.
npm i -D plop
npm i -g plop
Then create a file named plopfile.js
at the root of your project. This will hold all of the logic for our component generators. The plopfile exports a single function that takes the plop
object of methods. For now, we're only going to use setGenerator()
method, so we'll destructure that out for simplicity.
module.exports = function ({ setGenerator }) {
// Generator logic here
};
Create templates
Before writing our component generator, we need to create a boilerplate of templates, just like we did for our whole project with Yeoman. You can store your templates anywhere in your project, though we recommend keeping them separate to your src/
folder.
Plop uses handlebars for its templating engine. Handlebars uses {{ }}
characters to mark variables, and you can use helper methods for template logic. Plop also supplies a few additional helpers that are useful for generators.
Unlike Yeoman, where we made our answers available on a props
object, Plop will automatically pass answers from prompts to templates, so we can access them directly.
import React from 'react';
/**
* {{name}} component
* {{description}}
*/
export default function {{name}}({ {{#each props}}
{{this.name}} = {{{this.value}}},{{/each}}
className,
...attrs
}) {
return (
<div className={className} {...attrs} />
);
}
Writing a generator
Plop generators are very simple. Call setGenerator(name, { description, prompts, actions })
, provide prompts in the same format as Yeoman (Plop also uses inquirer.js), then define the actions it should take.
The action we'll be using is add
. It takes the destination path
of your component (you can use answers from your prompts in this path), and the template file to use. You can generate multiple files at once (eg: storybook configs, style modules) with addMany, and perform your own custom logic by passing a function rather than array to actions
.
We're also going to transform one of our answers using a filter function, to split the props we define into an array to use in templates. A filter takes the answer provided, and returns a new value for Plop to use.
function filterProps(props) {
const objectify = (prop) => {
let vals = prop.split(':');
return { name: vals[0], value: vals[1] };
};
return props
.split(',')
.map((propString) => propString.trim())
.map(objectify);
}
plop.setGenerator('component', {
description: 'Generate a new component',
prompts: [
{
type: 'input',
name: 'name',
message: 'Name',
},
{
type: 'input',
name: 'description',
message: 'Description',
},
{
type: 'input',
name: 'props',
message: 'Props :: [prop]:[value]',
filter: filterProps,
},
],
actions: [
{
type: 'add',
path: 'src/components/{{name}}/index.js',
templateFile: 'plop/component.js',
},
],
});
Running the generator
Simply call plop
in the root of your project and follow the prompts to generate a new, tailor-made React component for your project. You can go even further with Plop, follow their documentation for more.
Putting it all together
Now we're scaffolding our projects with Yeoman, and generating our components with Plop, why not put the two together and generate our generators? Since Yeoman and Plop use different templating engines, this part is easy. Just include your plopfile and generator templates in the Yeoman boilerplate we setup before. You can customise them with EJS just like we did with other files.
Then when you are ready to start your next project, you can scaffold your entire tailor-made boilerplate with one command, and have templates within that project ready to generate as well!