☕☕ 8 min read
Plop is a little node package that will ease your life whenever you need to create a new controller / router / helper / …
Hey, that’s damn easy to create a new controller: copy-paste another controller code, delete specific lines you won’t use. Tadaaa!
Well, now pop a bunch of legitimate questions:
Find a good file, open it, copy its content, create a new file, paste the content, delete useless lines… It’s repetitive, error prone and doesn’t have a lot of added value. And that could take some time. Most of all, that’s frequent!
It would just be awesome to write plop
in your terminal, answer 2 questions and BIM, you’re all set!
That’s exactly what I’m talking about here.
As a node package, npm install -g plop
and you’re ready to play with plop.
You also can install it locally to the project, adding it to dependencies: npm install --save-dev plop
.
Then add it to your package.json
scripts so you can run it with npm run plop
:
{
"name": "your-awesome-project",
"description": "This is an awesome project, isn't it?",
"dependencies": {},
"devDependencies": {
"plop": "1.0.1"
},
"scripts": {
"plop": "plop"
}
}
Voilà!
Plop uses a simple plopfile.js
.
Here’s the standard configuration file I’d suggest you to put at the root of the project:
module.exports = plop => {
// Here we'll define our generators
}
Plop will also use templates that can either be inlined within the configuration file, or put in separate files. My suggestion is to put them into a plop-templates/
folder, at the root of the project too.
As you’d guess: both generator and templates are embedded in the project, just like your tests or task runner (brunch, gulp, grunt…).
This has tremendous advantages over a customized Yeoman generator:
In a nutshell, when a project-specific Yeoman generator may sound overkill, plop fits perfectly. Lightweight, close to source code, it will be easier to adopt, maintain and, at the end, it will be used.
To declare a new generator, plop
provides you setGenerator
:
module.exports = plop => {
// We declare a new generator called "module"
plop.setGenerator('module', {
// Succintly describes what generator does.
description: 'Create a new module',
// Get inputs from the user.
// That's Inquirer.js doing the job behind the hood.
prompts: [
{
type: 'input',
name: 'name',
message: 'What is your module name?',
},
],
// List of actions to take.
// Here we "add" new files from our templates.
actions: [
{
type: 'add',
path: 'app/modules/{{camelCase name}}.js',
templateFile: 'plop-templates/module.js',
},
{
type: 'add',
path: 'app/tests/{{camelCase name}}.tests.js',
templateFile: 'plop-templates/module.tests.js',
},
],
})
}
The prompts
part is delegated to Inquirer.js.
You can just refer to their documentation to learn whatever you can do — questions types, output filter, input validation…
You can imagine doing some not trivial stuff:
import { trimRight, isEmpty } from 'lodash'
const ensurePlural = text => trimRight(text, 's') + 's'
const isNotEmptyFor = name => {
return value => {
if (isEmpty(value)) return name + ' is required'
return true
}
}
module.exports = plop => {
plop.setGenerator('module', {
// …
prompts: [
{
type: 'input',
name: 'name',
message: 'What is your module name?',
validate: isNotEmptyFor('name'),
filter: ensurePlural,
},
],
// …
})
}
validate
will ensure the given module name is not empty.
filter
allow you to standardize the output: here module names should end with an s
by convention. Even if I do it wrong and give calendar
as the module name, I can be sure the name
variable will be calendars
.
Now it knows everything it needs, plop will run every actions
you configured. It can use every variables that inquirer did transmit.
Actions, just like templates, are parsed with Handlebars. If you understand how it works, you know how to use plop.
Therefore {{name}}
is the answer given to the prompt, validated and filtered previously. You can drop it wherever needed, in the created file path and/or its template.
There are 2 types of actions that are supported yet:
"add"
that will create a new file into the given path
— which is relative to plopfile.js
"modify"
that will modify the file located at given path
. It will replace the RegExp you provided in pattern
with the template you provideBoth actions can either use an inlined template via template
, or retrieve it from the path you set via templateFile
.
Let’s imagine that kind of implementation:
const modulePath = 'app/modules/{{camelCase name}}.js'
module.exports = plop => {
plop.setGenerator('model', {
// …
actions: [
// Add a new model + tests boilerplate.
{
type: 'add',
path: 'app/modules/{{camelCase name}}.model.js',
templateFile: 'plop-templates/model.js',
},
{
type: 'add',
path: 'app/tests/{{camelCase name}}.model.tests.js',
templateFile: 'plop-templates/model.tests.js',
},
// Modify the module file to inject created model.
// This is basically RegExp replacement.
{
type: 'modify',
path: modulePath,
pattern: /(\/\/ IMPORT MODULE FILES)/g,
template: '$1\nimport Model from "./{{camelCase name}}.model";',
},
{
type: 'modify',
path: modulePath,
pattern: /(const namespace = "\w+";)/g,
template: '$1\n\nModel = Model.extend( { namespace: namespace } );',
},
],
})
}
Then, let’s say you have the plop-templates/model.js
template:
/**
* TODO - Describe what your model does.
*
* @class {{pascalCase name}}.Model
* @module {{pascalCase name}}
* @constructor
*/
import { Model } from 'backbone'
export default Model.extend({
initialize() {
// Executed on model initialization
},
})
Considering {{name}}
is calendars
, then plop will create the following app/modules/calendars.model.js
file:
/**
* TODO - Describe what your model does.
*
* @class Calendars.Model
* @module Calendars
* @constructor
*/
import { Model } from 'backbone'
export default Model.extend({
initialize() {
// Executed on model initialization
},
})
And will also transform your current app/modules/calendars.js
module file:
import Module from 'core/module'
import _ from 'lodash'
// IMPORT MODULE FILES
const namespace = 'calendars'
export default Module.extend({
initialize() {
_.defaults(this.options, { isDisplayed: true })
},
onStart() {
this.ready()
},
onReady() {
// Do something when module is considered as ready
},
})
To insert the reference to the created model:
import Module from 'core/module'
import _ from 'lodash'
// IMPORT MODULE FILES
import Model from './calendars.model'
const namespace = 'calendars'
Model = Model.extend({ namespace: namespace })
export default Module.extend({
initialize() {
_.defaults(this.options, { isDisplayed: true })
},
onStart() {
this.ready()
},
onReady() {
// Do something when module is considered as ready
},
})
With "add"
and "modify"
you can ease a lot of small repetitive things.
You can pass a function to actions
. This function will take user’s answers as a parameter and should return the array of actions to take.
This way you can adapt actions to given answers.
Let’s consider this example of new module creation:
module.exports = plop => {
plop.setGenerator('module', {
prompts: [
{
type: 'input',
name: 'name',
message: 'What is the name of your module?',
validate: isNotEmptyFor('name'),
filter: ensurePlural,
},
{
type: 'list',
name: 'dataConfig',
message: 'Tell me about the data, what do you need?',
default: 'none',
choices: [
{ name: 'Nothing', value: 'none' },
{ name: 'A Model', value: 'model' },
],
},
],
actions: data => {
// Add a new module, whatever happens.
let actions = [
{
type: 'add',
path: 'app/modules/{{camelCase name}}/{{camelCase name}}.js',
templateFile: 'plop-templates/module.js',
},
{
type: 'add',
path:
'app/modules/{{camelCase name}}/tests/{{camelCase name}}.tests.js',
templateFile: 'plop-templates/module.tests.js',
},
]
// If you wish a Model, then we add a Model.
if (data.dataConfig === 'model') {
actions = actions.concat([
{
type: 'add',
path: 'app/modules/{{camelCase name}}.model.js',
templateFile: 'plop-templates/model.js',
},
{
type: 'add',
path: 'app/tests/{{camelCase name}}.model.tests.js',
templateFile: 'plop-templates/model.tests.js',
},
])
}
// Return the array of actions to take.
return actions
},
})
}
Your generator can adapt to many different scenarios (e.g: a module with a Model, a Collection + Model, with a CollectionView or a CompositeView…).
A little note about templating helpers of plop: these are those from Handlebars.
You starts with a bunch of helpers plop gives you. camelCase
, for instance, just works this way: {{camelCase name}}
with name = "my awesome module"
gives "myAwesomeModule"
.
You can define your own helpers within plopfile.js
with addHelper
:
module.exports = plop => {
plop.addHelper('upperCase', text => text.toUpperCase())
// …
}
We just created an upperCase
helper we could use later in the actions
and templates: {{upperCase name}}
.
There you go, just type npm run plop
— or simply plop
, if you made it global — then follow the guide.
You can also directly call a specific generator with npm run plop [generatorName]
.
Plop is fast and efficient to use, just like Yeoman. However, it’s far more lightweight and easier to maintain.
As of the time of writing, I use plop with my team on the Vinoga project. Its features perfectly match our use cases.
I did wrote a Yeoman generator for the project before. It was concretely unused by the team.
Definitely, plop is the kind of tool that saves you 10 minutes here and there, every single day. And running plop module
in your console, how cool is that \o/