Grunt (3) : Configurer les tâches
Avec le précédent article sur l’API Grunt, vous avez eu un aperçu de comment accéder à la configuration et aux fichiers. Dans cet article, vous allez mettre en pratique ces connaissances dans le cadre de la configuration des tâches.
A la fin de votre lecture, vous saurez comment configurer les tâches Grunt. Vous aborderez en particulier les multitâches, ou tâches à cibles multiples, qui offrent des fonctionnalités intéressantes pour la manipulation de fichiers. Dès lors, vous serez prêts pour aborder la création des tâches Grunt.
Que contient cet article ?
Cet article introduit d’abord quelques généralités sur la configuration des tâches :
Passées ces généralités, nous entrerons dans le détail de la configuration.
D’abord ce qu’offre Grunt en terme de configuration pour toutes les tâches :
Puis, ce qui est spécifique aux multitâches :
Configurer une tâche
Comment ?
Dans tout code JavaScript où l’objet grunt existe, il est possible d’accéder à l’objet de configuration. Je vous renvoie aux articles précédents qui introduisent ce concept : Grunt : The JavaScript Task Runner, L’API Grunt.
//Initialisation de la configuration
grunt.initConfig({
uglify: {
options: {
banner: '/*Mon package : <%=grunt.template.today("yyyy-mm-dd")%>*/\n'
},
build: {
src: 'source/monPackage.js',
dest: 'target/monPackage.min.js'
}
}
});
//Modification de la configuration
grunt.config('uglify.build.src', 'src/mon-package.js');
grunt.config('uglify.build.dest', 'dest/mon-package.min.js');
grunt.config('uglify.build.newProperty', 123);
//Accès à une propriété
grunt.registerTask('default', function() {
console.log(grunt.config('uglify.build'));
});
Pourquoi ?
Mais pourquoi se prendre la tête avec de la configuration puisqu’il est tout à fait possible de créer une tâche fonctionnelle qui s’en passe ?
module.exports = function(grunt) {
grunt.registerTask('file-copy', function() {
var srcFiles = [
"./src/file-1.txt",
"./src/file-2.txt",
"./src/file-3.txt"
];
var destFiles = [
"./dest/file-1-copy.txt",
"./dest/file-2-copy.txt",
"./dest/file-3-copy.txt"
];
for (var i=0; i<srcFiles.length; i++){
try {
grunt.file.copy(srcFiles[i], destFiles[i]);
}
catch(e){
console.log(e.message);
}
}
});
};
L’intérêt de Grunt est l’automatisation de tâches récurrentes. Les tâches sont souvent les mêmes d’un projet à l’autre – voire apparaissent plusieurs fois au sein d’un même projet – mais le contexte est propre à chaque situation. Configurer une tâche, c’est lui donner un niveau d’abstraction suffisant pour pouvoir être réutilisée dans un autre contexte.
Configurer une tâche permet également d’en améliorer la lisibilité en séparant les données manipulées de la logique de la tâche.
Si nous reprenons l’exemple précédent :
module.exports = function(grunt) {
//Initialisation de la configuration
grunt.initConfig({
filecopy: {
srcFiles: [
"./src/file-1.txt",
"./src/file-2.txt",
"./src/file-3.txt"
],
destFiles: [
"./dest/file-1-copy.txt",
"./dest/file-2-copy.txt",
"./dest/file-3-copy.txt"
]
}
});
//La tâche filecopy utilise la configuration
grunt.registerTask('filecopy', function() {
var srcFiles = grunt.config("filecopy.srcFiles");
var destFiles = grunt.config("filecopy.destFiles");
for (var i=0; i>srcFiles.length; i++){
try {
grunt.file.copy(srcFiles[i], destFiles[i]);
}
catch(e){
console.log(e.message);
}
}
});
};
Une tâche configurable pourra être packagée afin d’être utilisée dans un autre projet. C’est le principe des tâches-tiers mises à disposition par la communauté et que vous pouvez charger et utiliser dans vos projets.
Convention de nommage des tâches
Le nom d’une tâche est soumis à la convention de nommage suivante :
grunt-contrib-<nom de la tâche>
: plugin officiellement supporté par l’équipe Gruntgrunt-<nom de la tâche>
: plugin créé par la communauté<nom de la tâche>
: tâche personnelle
Si une propriété de premier niveau de l’objet de configuration porte le même nom qu’une tâche déclarée, cette propriété est alors associée à cette tâche.
Vous pouvez nommer les tâches que vous créez pour vos projets comme vous le souhaitez tant que le nom de votre tâche correspond à un nom de propriété JavaScript valide, car je vous rappelle que Grunt cherchera une propriété portant le nom de votre tâche dans l’objet de configuration.
Structure de l’objet de configuration
L’objet de configuration peut contenir des propriétés nommées comme les tâches, mais également n’importe quelle donnée arbitraire pour autant qu’elle n’entre pas en conflit avec une propriété requise pour l’une des tâches.
La valeur d’une propriété n’est pas limitée aux objets JSON. Tout code JavaScript valide peut être utilisé. La configuration peut même être générée par programme.
La structure globale de la configuration est la suivante :
grunt.initConfig({
simple_task: {
// Configuration de la tâche "simple_task".
},
multi_task: {
// Configuration de la tâche "multi_task".
first_target : {
// Configuration de la cible "first_target"
},
second_target : {
// Configuration de la cible "second_target"
}
},
// Propriétés arbitraires non-spécifiques à une tâche.
my_first_property: 'whatever',
my_second_property: { /* Objet JSON */ },
my_third_property : [ /* Tableau */ ]
});
Toutes les tâches
Hash
La configuration spécifique d’une tâche correspond à la valeur de la propriété portant le nom de la tâche dans l’objet de configuration de Grunt. La valeur attendue est une table de hachage : une association clé-valeur sous la forme d’un objet JavaScript. Si ce n’est pas une table de hachage cela peut quand même fonctionner, mais vous aurez quelques surprises en utilisant l’API Grunt…
grunt.initConfig({
simple_task: {
prop1: 'Et',
prop2: 1000,
prop3 : '&',
prop4: ['I', 'm', 'a', 'g', 'e', 's'],
prop5 : { disco: 'Funk' }
}
});
Toute propriété de la configuration de la tâche est accessible via l’API Grunt avec les méthodes grunt.config
, grunt.config.get
et grunt.config.getRaw
:
grunt.registerTask('simple_task', function() {
console.log(grunt.config("simple_task.prop1"));
console.log(grunt.config.get("simple_task.prop2"));
console.log(grunt.config.getRaw("simple_task.prop4"));
});
Options
Il est possible de mettre ce que l’on veut dans la configuration d’une tâche et d’y accéder via l’API dans une tâche. Par contre, les méthodes grunt.config
, grunt.config.get
et grunt.config.getRaw
obligent à spécifier tout le chemin pour accéder à une propriété d’une tâche alors qu’il serait préférable de ne commencer la navigation qu’à partir de la configuration spécifique de la tâche.
Grunt permet de le faire via la propriété options
de la configuration spécifique d’une tâche et la méthode this.options
de l’API :
module.exports = function(grunt) {
//Initialisation de la configuration
grunt.initConfig({
//Configuration spécifique de la tâche "simple_task"
simple_task: {
//Options de la tâche
options: {
prop1: 'Et',
prop2: 1000,
prop3 : '&',
prop4: ['I', 'm', 'a', 'g', 'e', 's'],
prop5 : { disco: 'Funk' }
},
prop6: 'une autre propriété...'
},
prop7: 'une donnée arbitraire non liée à une tâche...'
});
grunt.registerTask('simple_task', function() {
console.log("\n");
console.log(this.options()); // accès à tout l'objet options
console.log("\n");
console.log(this.options().prop5); // accès à une propriété
});
}
De plus, Grunt propose un système de surcharge qui permet de définir des valeurs par défaut aux propriétés de l’objet options
dans le corps de la tâche et ces valeurs sont remplacées par celle de la configuration si elles existent :
module.exports = function(grunt) {
//Initialisation de la configuration
grunt.initConfig({
//Configuration spécifique de la tâche "simple_task"
simple_task: {
//Options de la tâche
options: {
prop1: 'Et',
prop2: 1000,
prop3 : '&',
prop4: ['I', 'm', 'a', 'g', 'e', 's']
},
prop6: 'une autre propriété...'
},
prop7: 'une donnée arbitraire non liée à une tâche...'
});
grunt.registerTask('simple_task', function() {
// Configuration par défaut
var options = this.options({
new_property: 22/7,
prop1: 'É',
prop5 : { disco: 'Funk' }
});
console.log(options);
});
}
La configuration par défaut de la tâche a bien été surchargée par la configuration initialisée avec grunt.initConfig
:
new_property
etprop5
n’ont pas été surchargées et conservent leur valeur par défautprop1
est redéfinie et sa valeur passe de'É'
à'Et'
Templates
Un template, spécifié par les délimiteurs <%= %>
, est évalué récursivement et automatiquement :
- quand il est lu dans la configuration avec les méthodes
grunt.config
etgrunt.config.get
- dans le contexte d’une tâche via
this.options
- dans le contexte d’une tâche à cibles multiples via
this.files
etthis.filesSrc
.
Toutes ces méthodes appellent en interne la méthode grunt.template.process
pour résoudre chaque template récursivement.
Un template peut s’exprimer comme :
- La valeur d’une autre propriété ou sous-propriété de l’objet de configuration. Dans ce cas, l’objet de configuration est le contexte dans lequel les valeurs des propriétés peuvent être résolues. L’évaluation étant récursive, si la propriété référencée dans le template renvoie un template, celui-ci est également évalué.
- Du code JavaScript valide arbitraire : un appel à une méthode de base du JavaScript ou de l’objet grunt, un appel à du code que vous avez écrit, etc.
Un template est toujours entre quote ou double-quote. Cependant, la valeur retournée à l’évaluation est du type de l’expression passée dans le template (string, int, objet, tableau…).
module.exports = function(grunt) {
grunt.initConfig({
// Donnée arbitraire
value: 123.45,
// Configuration de "first_task"
first_task: {
options: {
value: "<%= value %>",
today: "<%= grunt.template.today('yyyy-mm-dd') %>"
}
},
// Configuration de "second_task"
second_task: {
options: {
value: "<%= first_task.options.value %>"
}
}
});
grunt.registerTask('first_task', function() {
console.log(grunt.config("value")); // 123.45
console.log(this.options().value); // 123.45
console.log(this.options().today); // 2014-10-01
});
grunt.registerTask('second_task', function() {
console.log(this.options().value * 2); // 246.9
/*
initialement : "<%= first_task.options.value %>" * 2
évaluation 1 : "<%= value %>" * 2
évaluation 2 : 123.45 * 2 = 246.9
*/
});
}
Données externes
Grâce à l’API Grunt, il est possible de lire des fichiers. Les méthodes de grunt.file
peuvent être appelées dans les tâches, mais aussi dans la configuration grâce aux templates.
Les méthodes grunt.file.readJSON
et grunt.file.readYAML
permettent d’importer directement des données JSON ou YAML.
// data.json
{
name: 'My package',
description: 'This is my package which does so much things.',
prefix: 'my.package',
version: '2.1.0'
}
grunt.initConfig({
pck: '<%= grunt.file.readJSON('data.json') %>',
minify: {
name: '<%= pck.name %>',
description: '<%= pck.description %>',
src: '<%= pck.prefix %>-<%= pck.version %>.js',
dest: '<%= pck.prefix %>-<%= pck.version %>.min.js',
}
});
Multitâche ou tâche “à cibles multiples”
Les multitâches, en plus de bénéficier des possibilités de configuration énoncées précédemment, disposent de mécanismes supplémentaires pour gérer les options et les fichiers. Mais tout d’abord, qu’est-ce qu’une multitâche ?
Targets
Au lancement d’une tâche, Grunt cherche une propriété de configuration au nom de la tâche. Les multitâches, elles, peuvent avoir plusieurs sous-configurations appelées cibles ou “targets”. Si une target est spécifiée, la tâche s’exécute avec la configuration de la target. Si aucune target n’est spécifiées, la tâche est exécutée pour chaque target.
Prenons comme exemple une tâche compile dont la configuration varie en fonction de l’environnement de travail :
grunt.initConfig({
compile: {
dev: {
// Configuration pour le développement
},
test: {
// Configuration pour l'intégration
},
uat: {
// Configuration pour l'UAT
},
preprod: {
// Configuration pour la pré-production
}
prod: {
// Configuration pour la production
}
}
});
La tâche compile:dev
est exécutée avec la configuration de développement, alors que la tâche compile:prod
s’exécutera avec la configuration de production. Par contre, exécuter la tâche compile
revient à exécuter séquentiellement les tâches compile:dev
, compile:test
, compile:uat
, compile:preprod
et compile:prod.
Options
Nous avons vu plus tôt l’objet de configuration options
accessible dans le contexte d’une tâche via la méthode this.options
. Un objet options
par défaut peut être spécifié dans le code de la tâche et ses propriétés sont surchargées par celles de l’objet options
de la configuration de la tâche.
Avec les targets, il y a un niveau supplémentaire de surcharge :
- Implémentation de la tâche : options par défaut
- Configuration de la tâche : surcharge (1)
- Configuration de la target : surchage (2)
module.exports = function(grunt) {
grunt.initConfig({
multi_task: {
// options au niveau de la tâche "multi_task"
options: {
foo: 0,
bar: "highfive"
},
first_target: {
// options au niveau de la target "first_target"
options: {
foo: 1,
oque: false
}
},
second_target: {
// pas d'options au niveau de la target "second_target"
}
}
});
grunt.registerMultiTask("multi_task", "une tâche à cibles multiples", function() {
var options = this.options({
foo: 2,
bar: "clap your hands !",
oque: true
});
console.log(options);
});
}
Fichiers
Il y a trois façons de décrire les correspondances de fichiers source-destination. N’importe quelle tâches saura interpréter ces formats, donc à vous de choisir celui qui convient le mieux à vos besoins. Dans le contexte d’une tâche, les correspondances de fichier source-destination sont accessibles via le tableau d’objets this.files
.
Si les fichiers ciblés sont peu nombreux et clairement identifiés, il est simple de spécifier tous les chemins. Comme ce n’est généralement pas le cas, Grunt supporte un certain nombre de motifs via les packages node-glob et minimatch.
Motifs
Les caractères génériques usuels :
*
correspond à un nombre quelconque de caractères à l’exception de/
'f*' // correspond aussi bien à 'f', 'fo', 'foo' ou 'foo.js', mais pas à 'f/'
?
correspond à un caractère à l’exception de/
'f?' // correspond aussi bien à 'fa' ou 'fo', mais ni à 'f/', ni à 'foo'
**
correspond à un nombre quelconque de caractères,/
inclus tant qu’il est l’unique caractère d’un segment du chemin'f**' // correspond aussi bien à 'f', 'foo.js' ou 'foo/bar.js'
{}
délimite une liste d’expression ‘OU’ à séparateur virgule'{foo,bar}.js' // correspond à 'foo.js' ou 'bar.js'
!
au début d’un motif correspond à la négation du motif'!{foo,bar}.js' // correspond à tout fichier .js sauf 'foo.js' et 'bar.js'
Format compact
Les associations src-dest sont représentées sous la forme de deux propriétés src
et dest
. Ce format est couramment utilisé pour les tâches “lecture seule” où seule la propriété src
est requise.
grunt.initConfig({
jshint: {
foo: {
src: ['src/aa.js', 'src/aaa.js']
}
},
concat: {
bar: {
src: ['src/bb.js', 'src/bbb.js'],
dest: 'dest/b.js'
}
}
});
Format objet
Les associations src-dest sont représentées par un objet files
. Ses propriétés sont les destinations et leur valeur, un tableau de sources.
grunt.initConfig({
concat: {
foo: {
files: {
'dest/a.js': ['src/aa.js', 'src/aaa.js'],
'dest/a1.js': ['src/aa1.js', 'src/aaa1.js']
}
},
bar: {
files: {
'dest/b.js': ['src/bb.js', 'src/bbb.js'],
'dest/b1.js': ['src/bb1.js', 'src/bbb1.js']
}
}
}
});
Format tableau
Les associations src-dest sont représentées par un tableau files
. Chaque élément du tableau est un objet au format compact.
grunt.initConfig({
concat: {
foo: {
files: [
{src: ['src/aa.js', 'src/aaa.js'], dest: 'dest/a.js'},
{src: ['src/aa1.js', 'src/aaa1.js'], dest: 'dest/a1.js'}
]
},
bar: {
files: [
{src: ['src/bb.js', 'src/bbb.js'], dest: 'dest/b/', nonull: true},
{src: ['src/bb1.js', 'src/bbb1.js'], dest: 'dest/b1/', filter: 'isFile'}
]
}
}
});
Propriétés additionnelles
Le format compact supporte d’autres propriétés que src
et dest
. Le format tableau étant un dérivé du format compact, il supporte également ces propriétés.
filter
string ou function : un nom de méthode fs.Stats valide ou une fonction prenant en argument un chemin de fichier et retournant un booléen.nonull
bool : Sitrue
, conserve les motifs même s’il n’y a aucune correspondance.dot
bool : Sitrue
, autorise les motifs à remonter les fichiers commençant par un.
, même si le motif ne le permet pas explicitement.matchBase
bool : Sitrue
, les motifs sans/
fonctionnent comme s’ils étaient préfixés de**/
expand
bool : Sitrue
, autorise la génération dynamique de la correspondance de fichiers
Pour gérer un grand nombre de fichiers, il est possible de construire dynamiquement la liste de fichiers en mettant la propriété expand
à true
. Dès lors, il est possible d’utiliser les propriétés suivantes :
cwd
string : Toutes les correspondances de sourcessrc
sont relatives à ce chemin sans l’inclure.src
string array : Motif(s) de correspondance, relatifs àcwd
.dest
string : Préfixe du chemin de destination.ext
string : Remplace toute extension existante par cette valeur dans les chemins de destination générés.extDot
string : Indique où le point de l’extension se situe. Peut prendre les valeurs'first'
ou'last'
, et est mis par défaut à'first'
.'first'
signifie que l’extension commence après le premier point du nom de fichier ;'last'
, après le dernier point.flatten
bool : Supprime toute l’arborescence de destinationdest
générée.rename
function : Appelée pour chaque fichiersrc
après le renommage de l’extension et l’aplanissement (cf.ext
etflatten
). Ledest
et le cheminsrc
correspondant sont passés à la fonction et la fonction doit retourner une nouvelle valeurdest
. Si le mêmedest
est retourné plusieurs fois, chaquesrc
qui correspond sera ajouté à un tableau de source pour cette destination.
D’autres propriétés sont supportées. Pour une liste exhaustive, voir la documentation des packages node-glob et minimatch.
Prenons l’arborescence de répertoires et de fichiers suivante :
La tâche sample affiche les sources, et pour la target expand les destinations.
module.exports = function(grunt) {
grunt.initConfig({
sample: {
filter: {
src: [ 'src/**/*' ], // motif sur la totalité de l arborescence src
filter: 'isFile' // filtre sur les fichiers
},
nonull: {
src: [ 'unknown.js' ], // motif sans correspondance
nonull: true // le motif remonte quand même dans this.files
},
expand: {
files: [
{
expand: true, // activation de la correspondance dynamique
cwd: 'src/', // chemin de la source
src: ['**/*.js'], // motif des fichiers .js de l arborescence src
dot: true, // les fichiers commençant par . sont pris en compte
dest: 'dest/', // chemin de destination
ext: '.min.js', // remplace l extension '.min.js'
extDot: 'last', // si 'first', '.period.js' devient '.min.js'
flatten: false // si true, 'src/{folder(s)}/{file}.{ext}' devient
// 'dest/{file}.{ext}' au lieu de 'dest/{folder(s)}/{file}.{ext}'
}
]
}
}
});
grunt.registerMultiTask("sample", "Gestion des fichiers", function() {
this.files.forEach(function(file) {
if (this.target == "expand"){
console.log(JSON.stringify(file.src) + " >> " + file.dest);
}
else{
console.log(file.src);
}
}.bind(this));
});
}
Et après ?
Dans cet article, vous avez pu mettre en application une partie de vos acquis précédent sur l’API Grunt. Vous avez appris à configurer les tâches Grunt, en particulier pour la gestion des fichiers en mode src-dest, source-destination.
Le prochain article de la série consacrée à Grunt abordera la création de tâches Grunt !