I. Que contient ce tutoriel ?▲
Ce tutoriel introduit d'abord quelques généralités sur la configuration des tâches :
- Comment et pourquoi configurer une tâche ?Configurer une tâche
- Comment se structure cette configuration ?Structure de l'objet de configuration
Passées ces généralités, nous entrerons dans le détail de la configuration.
D'abord ce qu'offre Grunt en termes de configuration pour toutes les tâches :
- La configuration comme hash de donnéesToutes les tâches
- Les options (1)Options
- Les templatesTemplates
- Charger des données externesDonnées externes
Puis, ce qui est spécifique aux multitâches :
II. Configurer une tâche▲
II-A. 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
//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'
));
}
);
II-B. 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 ?
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
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.
II-C. 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 Grunt ;
- grunt-<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.
II-D. 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
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 */
]
}
);
III. Toutes les tâches▲
III-A. 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.
2.
3.
4.
5.
6.
7.
8.
9.
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 :
2.
3.
4.
5.
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"
));
}
);
III-B. 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
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 celles de la configuration si elles existent :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
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 et prop5 n'ont pas été surchargées et conservent leur valeur par défaut ;
- prop1 est redéfinie et sa valeur passe de 'É' à 'Et'.
III-C. 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 et grunt.config.get ;
- dans le contexte d'une tâche via this.options ;
- dans le contexte d'une tâche à cibles multiples via this.files et this.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 quotes ou doubles-quotes. Cependant, la valeur retournée à l'évaluation est du type de l'expression passée dans le template (string, int, objet, tableau…).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
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
*/
}
);
}
III-D. 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.
2.
3.
4.
5.
6.
7.
// data.json
{
name
:
'My package'
,
description
:
'This is my package which does so much things.'
,
prefix
:
'my.package'
,
version
:
'2.1.0'
}
2.
3.
4.
5.
6.
7.
8.
9.
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'
,
}
}
);
IV. 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 ?
IV-A. 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ée, 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
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.
IV-B. Options▲
Nous avons vu plus tôt l'objet de configuration optionsOptions 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)
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
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'option 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);
}
);
}
IV-C. Fichiers▲
Il y a trois façons de décrire les correspondances de fichiers source-destination. N'importe quelle tâche 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.
IV-C-1. Motifs▲
Les caractères génériques usuels :
-
* correspond à un nombre quelconque de caractères à l'exception de /
Sélectionnez'f*'
// correspond aussi bien à 'f', 'fo', 'foo' ou 'foo.js', mais pas à 'f/'
-
? correspond à un caractère à l'exception de /
Sélectionnez'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
Sélectionnez'f**'
// correspond aussi bien à 'f', 'foo.js' ou 'foo/bar.js'
-
{} délimite une liste d'expression 'OU' à séparateur virgule
Sélectionnez'{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'
IV-C-2. 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
grunt.initConfig
({
jshint
:
{
foo
:
{
src
:
[
'src/aa.js'
,
'src/aaa.js'
]
}
},
concat
:
{
bar
:
{
src
:
[
'src/bb.js'
,
'src/bbb.js'
],
dest
:
'dest/b.js'
}
}
}
);
IV-C-3. 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
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'
]
}
}
}
}
);
IV-C-4. 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
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'
}
]
}
}
}
);
IV-C-5. 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 : Si true, conserve les motifs, même s'il n'y a aucune correspondance.
- dot bool : Si true, autorise les motifs à remonter les fichiers commençant par un ., même si le motif ne le permet pas explicitement.
- matchBase bool : Si true, les motifs sans / fonctionnent comme s'ils étaient préfixés de **/.
- expand bool : Si true, 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 sources src 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 destination dest générée ;
- rename function : appelée pour chaque fichier src après le renommage de l'extension et l'aplanissement (cf. ext et flatten). Le dest et le chemin src correspondant sont passés à la fonction et la fonction doit retourner une nouvelle valeur dest. Si le même dest est retourné plusieurs fois, chaque src 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
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));
}
);
}
V. Et après ?▲
Dans ce tutoriel, vous avez pu mettre en application une partie de vos acquis précédents 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 tutoriel de la série consacrée à Grunt abordera la création de tâches Grunt !
VI. Remerciements▲
Nous remercions la société SOAT qui nous a autorisés à publier ce tutoriel.
Nous remercions également Winjerome pour la mise au gabarit et Claude Leloup pour la relecture orthographique.