Tutoriel pour apprendre à utiliser l'outil JavaScript Grunt - Configurer les tâches

Nourdine FALOLA (Soat)

Avec le précédent tutoriel sur l'API Grunt, vous avez eu un aperçu de comment accéder à la configuration et aux fichiers. Dans ce tutoriel, vous allez mettre en pratique ces connaissances dans le cadre de la configuration des tâches.

À 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êt pour aborder la création des tâches Grunt.

Commentez

- Titre : Tutoriel pour apprendre à utiliser l'outil JavaScript Grunt - Configurer les tâches

- Auteur : Nourdine FALOLA (Soat)

- Parution : 18 avril 2017

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2016 Nourdine FALOLA. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Que contient ce tutoriel ?

- II. Configurer une tâche

- II-A. Comment ?

- II-B. Pourquoi ?

- II-C. Convention de nommage des tâches

- II-D. Structure de l'objet de configuration

- III. Toutes les tâches

- III-A. Hash

- III-B. Options

- III-C. Templates

- III-D. Données externes

- IV. Multitâche ou tâche « à cibles multiples »

- IV-A. Targets

- IV-B. Options

- IV-C. Fichiers

- IV-C-1. Motifs

- IV-C-2. Format compact

- IV-C-3. Format objet

- IV-C-4. Format tableau

- IV-C-5. Propriétés additionnelles

- V. Et après ?

- VI. Remerciements

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 ?

- Comment se structure cette 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ées

- Les options (1)

- Les templates

- Charger des données externes

Puis, ce qui est spécifique aux multitâches :

- Les targets

- Les options (2)

- Les fichiers

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.

1. //Initialisation de la configuration

2. grunt.initConfig({

3. uglify: {

4. options: {

5. banner: '/*Mon package : <%=grunt.template.today("yyyy-mm-dd")%>*/\n'

6. },

7. build: {

8. src: 'source/monPackage.js',

9. dest: 'target/monPackage.min.js'

10. }

11. }

12. });

14. //Modification de la configuration

15. grunt.config('uglify.build.src', 'src/mon-package.js');

16. grunt.config('uglify.build.dest', 'dest/mon-package.min.js');

17. grunt.config('uglify.build.newProperty', 123);

19. //Accès à une propriété

20. grunt.registerTask('default', function() {

21. console.log(grunt.config('uglify.build'));

22. });

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 ?

1. module.exports = function(grunt) {

2. grunt.registerTask('file-copy', function() {

3. var srcFiles = [

4. "./src/file-1.txt",

5. "./src/file-2.txt",

6. "./src/file-3.txt"

7. ];

9. var destFiles = [

10. "./dest/file-1-copy.txt",

11. "./dest/file-2-copy.txt",

12. "./dest/file-3-copy.txt"

13. ];

15. for (var i=0; i<srcFiles.length; i++){

16. try  {

17. grunt.file.copy(srcFiles[i], destFiles[i]);

18. }

19. catch(e){

20. console.log(e.message);

21. }

22. }

23. });

24. };

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 :

1. module.exports = function(grunt) {

2. //Initialisation de la configuration

3. grunt.initConfig({

4. filecopy: {

5. srcFiles: [

6. "./src/file-1.txt",

7. "./src/file-2.txt",

8. "./src/file-3.txt"

9. ],

10. destFiles: [

11. "./dest/file-1-copy.txt",

12. "./dest/file-2-copy.txt",

13. "./dest/file-3-copy.txt"

14. ]

15. }

16. });

18. //La tâche filecopy utilise la configuration

19. grunt.registerTask('filecopy', function() {

20. var srcFiles = grunt.config("filecopy.srcFiles");

21. var destFiles = grunt.config("filecopy.destFiles");

23. for (var i=0; i>srcFiles.length; i++){

24. try  {

25. grunt.file.copy(srcFiles[i], destFiles[i]);

26. }

27. catch(e){

28. console.log(e.message);

29. }

30. }

31. });

32. };

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 :

1. grunt.initConfig({

2. simple_task: {

3. // Configuration de la tâche "simple_task".

4. },

5. multi_task: {

6. // Configuration de la tâche "multi_task".

7. first_target : {

8. // Configuration de la cible "first_target"

9. },

10. second_target : {

11. // Configuration de la cible "second_target"

12. }

13. },

14. // Propriétés arbitraires non spécifiques à une tâche.

15. my_first_property: 'whatever',

16. my_second_property: { /* Objet JSON */ },

17. my_third_property : [ /* Tableau */ ]

18. });

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.

1. grunt.initConfig({

2. simple_task: {

3. prop1: 'Et',

4. prop2: 1000,

5. prop3 : '&',

6. prop4: ['I', 'm', 'a', 'g', 'e', 's'],

7. prop5 : { disco: 'Funk' }

8. }

9. });

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 :

1. grunt.registerTask('simple_task', function() {

2. console.log(grunt.config("simple_task.prop1"));

3. console.log(grunt.config.get("simple_task.prop2"));

4. console.log(grunt.config.getRaw("simple_task.prop4"));

5. });

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 :

1. module.exports = function(grunt) {

2. //Initialisation de la configuration

3. grunt.initConfig({

4. //Configuration spécifique de la tâche "simple_task"

5. simple_task: {

6. //Options de la tâche

7. options: {

8. prop1: 'Et',

9. prop2: 1000,

10. prop3 : '&',

11. prop4: ['I', 'm', 'a', 'g', 'e', 's'],

12. prop5 : { disco: 'Funk' }

13. },

14. prop6: 'une autre propriété...'

15. },

16. prop7: 'une donnée arbitraire non liée à une tâche...'

17. });

19. grunt.registerTask('simple_task', function() {

20. console.log("\n");

21. console.log(this.options()); // accès à tout l'objet options

22. console.log("\n");

23. console.log(this.options().prop5); // accès à une propriété

24. });

25. }

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 :

1. module.exports = function(grunt) {

2. //Initialisation de la configuration

3. grunt.initConfig({

4. //Configuration spécifique de la tâche "simple_task"

5. simple_task: {

6. //Options de la tâche

7. options: {

8. prop1: 'Et',

9. prop2: 1000,

10. prop3 : '&',

11. prop4: ['I', 'm', 'a', 'g', 'e', 's']

12. },

13. prop6: 'une autre propriété...'

14. },

15. prop7: 'une donnée arbitraire non liée à une tâche...'

16. });

18. grunt.registerTask('simple_task', function() {

19. // Configuration par défaut

20. var options = this.options({

21. new_property: 22/7,

22. prop1: 'É',

23. prop5 : { disco: 'Funk' }

24. });

26. console.log(options);

27. });

28. }

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…).

1. module.exports = function(grunt) {

2. grunt.initConfig({

3. // Donnée arbitraire

4. value: 123.45,

5. // Configuration de "first_task"

6. first_task: {

7. options: {

8. value: "<%= value %>",

9. today: "<%= grunt.template.today('yyyy-mm-dd') %>"

10. }

11. },

12. // Configuration de "second_task"

13. second_task: {

14. options: {

15. value: "<%= first_task.options.value %>"

16. }

17. }

18. });

20. grunt.registerTask('first_task', function() {

21. console.log(grunt.config("value")); // 123.45

22. console.log(this.options().value);  // 123.45

23. console.log(this.options().today);  // 2014-10-01

24. });

26. grunt.registerTask('second_task', function() {

27. console.log(this.options().value * 2);  // 246.9

28. /*

29. initialement : "<%= first_task.options.value %>" * 2

30. évaluation 1 : "<%= value %>" * 2

31. évaluation 2 : 123.45 * 2 = 246.9

32. */

33. });

34. }

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.

1. // data.json

2. {

3. name: 'My package',

4. description: 'This is my package which does so much things.',

5. prefix: 'my.package',

6. version: '2.1.0'

7. }

1. grunt.initConfig({

2. pck: '<%= grunt.file.readJSON('data.json') %>',

3. minify: {

4. name: '<%= pck.name %>',

5. description: '<%= pck.description %>',

6. src: '<%= pck.prefix %>-<%= pck.version %>.js',

7. dest: '<%= pck.prefix %>-<%= pck.version %>.min.js',

8. }

9. });

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 :

1. grunt.initConfig({

2. compile: {

3. dev: {

4. // Configuration pour le développement

5. },

6. test: {

7. // Configuration pour l'intégration

8. },

9. uat: {

10. // Configuration pour l'UAT

11. },

12. preprod: {

13. // Configuration pour la préproduction

14. }

15. prod: {

16. // Configuration pour la production

17. }

18. }

19. });

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 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)

1. module.exports = function(grunt) {

2. grunt.initConfig({

3. multi_task: {

4. // options au niveau de la tâche "multi_task"

5. options: {

6. foo: 0,

7. bar: "highfive"

8. },

9. first_target: {

10. // options au niveau de la target "first_target"

11. options: {

12. foo: 1,

13. oque: false

14. }

15. },

16. second_target: {

17. // pas d'option au niveau de la target "second_target"

18. }

19. }

20. });

22. grunt.registerMultiTask("multi_task", "une tâche à cibles multiples", function() {

23. var options = this.options({

24. foo: 2,

25. bar: "clap your hands !",

26. oque: true

27. });

29. console.log(options);

30. });

31. }

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 /

'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'

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.

1. grunt.initConfig({

2. jshint: {

3. foo: {

4. src: ['src/aa.js', 'src/aaa.js']

5. }

6. },

7. concat: {

8. bar: {

9. src: ['src/bb.js', 'src/bbb.js'],

10. dest: 'dest/b.js'

11. }

12. }

13. });

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.

1. grunt.initConfig({

2. concat: {

3. foo: {

4. files: {

5. 'dest/a.js': ['src/aa.js', 'src/aaa.js'],

6. 'dest/a1.js': ['src/aa1.js', 'src/aaa1.js']

7. }

8. },

9. bar: {

10. files: {

11. 'dest/b.js': ['src/bb.js', 'src/bbb.js'],

12. 'dest/b1.js': ['src/bb1.js', 'src/bbb1.js']

13. }

14. }

15. }

16. });

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.

1. grunt.initConfig({

2. concat: {

3. foo: {

4. files: [

5. {src: ['src/aa.js', 'src/aaa.js'], dest: 'dest/a.js'},

6. {src: ['src/aa1.js', 'src/aaa1.js'], dest: 'dest/a1.js'}

7. ]

8. },

9. bar: {

10. files: [

11. {src: ['src/bb.js', 'src/bbb.js'], dest: 'dest/b/', nonull: true},

12. {src: ['src/bb1.js', 'src/bbb1.js'], dest: 'dest/b1/', filter: 'isFile'}

13. ]

14. }

15. }

16. });

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.

1. module.exports = function(grunt) {

2. grunt.initConfig({

3. sample: {

4. filter: {

5. src: [ 'src/**/*' ], // motif sur la totalité de l arborescence src

6. filter: 'isFile'     // filtre sur les fichiers

7. },

8. nonull: {

9. src: [ 'unknown.js' ], // motif sans correspondance

10. nonull: true           // le motif remonte quand même dans this.files

11. },

12. expand: {

13. files: [

14. {

15. expand: true,     // activation de la correspondance dynamique

16. cwd: 'src/',      // chemin de la source

17. src: ['**/*.js'], // motif des fichiers .js de l arborescence src

18. dot: true,        // les fichiers commençant par . sont pris en compte

19. dest: 'dest/',    // chemin de destination

20. ext: '.min.js',   // remplace l extension '.min.js'

21. extDot: 'last',   // si 'first', '.period.js' devient '.min.js'

22. flatten: false    // si true, 'src/{folder(s)}/{file}.{ext}' devient

23. // 'dest/{file}.{ext}' au lieu de 'dest/{folder(s)}/{file}.{ext}'

24. }

25. ]

26. }

27. }

28. });

30. grunt.registerMultiTask("sample", "Gestion des fichiers", function() {

31. this.files.forEach(function(file) {

32. if (this.target == "expand"){

33. console.log(JSON.stringify(file.src) + " >> " + file.dest);

34. }

35. else{

36. console.log(file.src);

37. }

38. }.bind(this));

39. });

40. }

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.