☕☕ 9 min de lecture
J’ai présenté ce talk le 21 décembre 2015 au meetup Node.js Paris Chapitre 3 / Conférence 2.
Plop c’est un petit paquet node qui permet de se simplifier la vie quand on veut créer un nouveau controller / router / helper / …
Bah créer un nouveau controller c’est facile : je copie-colle le code d’un autre controller et je supprime les lignes dont je ne me sers pas. Tadaaa !
Certes, mais viennent un certain nombre de questions légitimes :
Trouver un bon fichier, l’ouvrir, copier son contenu, créer un nouveau fichier, coller le contenu, supprimer les lignes inutiles… C’est répétitif, propice aux erreurs et ça n’a pas beaucoup de valeur ajoutée. En plus, ça peut prendre un peu de temps. Surtout, c’est fréquent !
Ce serait vachement mieux à la place d’écrire plop
dans son terminal, répondre à 2 questions et BIM, c’est fait !
C’est exactement ce que nous allons voir ici.
Plop étant un paquet node, npm install -g plop
et vous pouvez commencer à jouer.
On peut aussi l’installer localement sur le projet en le rajoutant aux dépendances : npm install --save-dev plop
.
Puis ajoutez-le aux scripts de votre package.json
pour pouvoir le lancer avec 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 se base sur un plopfile.js
.
C’est le fichier de configuration standard, que je vous suggère de placer à la racine du projet :
module.exports = plop => {
// C'est ici qu'on va configurer nos générateurs
}
Plop va également se baser sur des templates qui peuvent soit être inlined dans le fichier de configuration, soit placés dans des fichiers séparés. Je vous suggère de les placer dans un dossier plop-templates/
, à la racine du projet également.
Comme vous l’aurez compris : le générateur et les templates sont embarqués dans le projet, à l’instar des tests ou du task runner (brunch, gulp, grunt…).
Cela a des avantages considérables sur un générateur Yeoman personnalisé :
Finalement, là où un développer un générateur Yeoman spécifique au projet est overkill, plop est parfaitement adapté. Léger, près du code source, il sera plus facilement adopté, maintenu et, en fin de compte, utilisé.
Pour déclarer un générateur, plop
nous fournit la méthode setGenerator
:
module.exports = plop => {
// On déclare un nouveau générateur appelé "module"
plop.setGenerator('module', {
// Décrit succintement ce que fait le générateur
// pour s'y retrouver.
description: 'Create a new module',
// Récupère les inputs de l'utilisateur.
// C'est Inquirer.js qui fait le job ici.
prompts: [
{
type: 'input',
name: 'name',
message: 'What is your module name?',
},
],
// Liste des actions à faire.
// Ici, on "add" de nouveaux fichiers à partir
// de nos 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',
},
],
})
}
La partie prompts
est directement déléguée à Inquirer.js.
Vous pouvez donc vous référez à leur documentation pour découvrir tout ce que vous pouvez faire (type des questions, filtre d’output, validation d’input…).
On peut ainsi imaginer des choses un peu plus complexes :
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
va s’assurer que le nom donné pour le module n’est pas vide.
filter
me permet de formaliser l’output : tous les noms des modules doivent se terminer par un s
. Ainsi, si par inadvertance je nomme mon module calendar
, je suis assuré que la variable name
vaudra calendars
pour la suite.
Une fois qu’il sait tout, plop va réaliser l’ensemble des actions
qu’on lui demande. Il dispose à ce moment là des variables que lui fournit inquirer.
Les actions, comme les templates, sont parsées avec Handlebars. Si vous avez compris son fonctionnement, vous savez déjà utiliser plop.
Ainsi {{name}}
correspond à la réponse donnée au prompt, validée et filtrée au préalable. Il me suffit de la placer où bon me semble, dans le chemin du fichier créé et/ou son template.
Il faut savoir qu’il y a 2 types d’actions supportés pour le moment :
"add"
qui va créer un nouveau fichier au niveau du path
indiqué (relatif à plopfile.js
)"modify"
qui va modifier le fichier situé au niveau du path
. Il va remplacer la RegExp définie dans pattern
par le templatePour les 2 actions on peut soit utiliser un template inline via template
, soit spécifier le chemin du template à utiliser via templateFile
.
Ça peut donner quelque chose du genre :
const modulePath = 'app/modules/{{camelCase name}}.js'
module.exports = plop => {
plop.setGenerator('model', {
// …
actions: [
// Ajoute un nouveau model + boilerplate de tests.
{
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',
},
// Modifie le module pour y injecter le model créé.
// Tout fonctionne avec un replace de RegExp.
{
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 } );',
},
],
})
}
À partir du template plop-templates/model.js
:
/**
* 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
},
})
Si {{name}}
vaut calendars
, alors plop va créer le fichier app/modules/calendars.model.js
suivant :
/**
* 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
},
})
Et va transformer notre app/modules/calendars.js
actuel :
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
},
})
Pour y insérer le model créé :
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
},
})
Avec "add"
et "modify"
il est possible de faire un paquet de petites choses répétitives plus simplement.
Vous pouvez également passer une fonction à actions
. Cette fonction prend en paramètre les réponses de l’utilisateur et doit retourner le tableau des actions à effectuer.
L’intérêt c’est de pouvoir adapter les actions en fonction des réponses données.
Prenons l’exemple de la création d’un nouveau module :
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 => {
// Ajoute un nouveau module quoiqu'il en soit.
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',
},
]
// Si l'on souhaite un modèle, alors on en ajoute un
// dans la foulée.
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',
},
])
}
// Retourne le tableau des actions à réaliser.
return actions
},
})
}
Le générateur peut donc s’adapter aux réponses que l’on donne et prendre en compte un certain nombre de scénarios (un module avec un Model, une Collection + Model, avec une CollectionView ou bien une CompositeView…).
Un petit point sur les helpers de templating de plop : ce sont ceux de Handlebars.
Il y a déjà un certain nombre de helpers fournis par plop. camelCase
, par exemple, fonctionne ainsi : {{camelCase name}}
avec name = "my awesome module"
donne "myAwesomeModule"
.
Vous pouvez définir vos propres helpers dans le plopfile.js
avec addHelper
:
module.exports = plop => {
plop.addHelper('upperCase', text => text.toUpperCase())
// …
}
On vient de créer un helper upperCase
que l’on pourra utiliser dans les actions
et les templates : {{upperCase name}}
.
Et c’est tout, il ne reste plus qu’à lancer npm run plop
(ou plop
, si vous l’avez installé globalement) et se laisser guider.
On peut aussi directement appeler un générateur avec npm run plop [generatorName]
.
À l’usage plop est rapide et efficace tout comme Yeoman. Par contre, il est bien plus léger et simple à maintenir.
À l’heure actuelle, j’utilise plop avec mon équipe sur le projet Vinoga. Ses fonctionnalités conviennent parfaitement à nos use cases.
J’avais développé un générateur Yeoman auparavant, largement inutilisé par l’équipe en pratique.
Vraiment, plop est le genre d’outil qui nous fait gagner 10 minutes par-ci par-là au quotidien. Et lancer plop module
dans sa console, c’est plutôt cool \o/