JTB

Grunt: L’automatisation Facile (Part 2)

Nous avons vu dans l’article précédent Grunt: l’automatisation facile (part 1) comment installer et configurer Grunt. Nous allons dans aujourd’hui utiliser des plugins pour aller plus loin et créer nos propres tâches.

tous les codes source de cet article sont disponibles sous github

Les plugins

Grunt est bien supporté par la communauté, et la multitude de plugins disponibles pourront vous rendre de grands services.

Quelques exemples parmi les plus connus:

  • grunt-contrib-watch : permet de surveiller une arborescence de fichier de de déclencher des actions en cas de modification de l’un d’eux (voir exemples ci-dessous)
  • grunt-contrib-copy : comme le nom l’indique, permet de copier des fichiers d’un répertoire A à un répertoire B
  • grunt-contrib-concat : concatène plusieurs fichiers en un seul
  • grunt-contrib-uglify : “minifie” des fichiers en utilisant la librairie UglifyJS
  • grunt-contrib-jshint : linter javascript utilisant la librairie JSHint (voir exemple plus bas)
  • grunt-shell : permet de définir des tâches custom utilisant le shell (voir exemple plus bas)

Pour aller plus loin …

Pour ceux qui souhaitent approfondir un peu, vous trouverez ci-dessous quelques exemples complémentaires et un paragraphe sur l’utilisation de tâches personnalisées.

Quelques exemples d’utilisation des plugins

grunt-contrib-jshint

JSHint permet de détecter dans le code javascript des erreurs de syntaxe simples (oubli d’un ‘;’ à la fin d’une ligne, oubli d’une accolade à la fin d’un bloc, utilisation de noms de variable non conforme, …)

Il s’installe de la manière suivante:

1
% npm install grunt-contrib-jshint --save-dev

Et il se configure dans le Gruntfile suivant l’exemple ci-dessous, qui va vérifier le fichier Gruntfile.js lui-même, et tous les fichiers javascript se trouvant dans le répertoire js :

Gruntfile.js
1
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) {
  'use strict';

  // Project configuration.
  grunt.initConfig({

   jshint: {
      files: [
        'Gruntfile.js',
        'js/**/*.js'
      ],
      options: {
      }
    }

  });


  // Load the plugin that provides the "jshint" task.
  grunt.loadNpmTasks('grunt-contrib-jshint');

  // Default task(s).
  grunt.registerTask('default', ['jshint']);

};

Puis il vous suffit d’exécuter la commande grunt:

1
2
3
4
5
% grunt
Running "jshint:files" (jshint) task
>> 1 file lint free.

Done, without errors.

On voit que le fichier Gruntfile.js a été vérifié avec succès.

grunt-contrib-watch

Le plugin watch permet de déclencher des tâche lorsque des fichiers sont modifiés.

Il s’installe de la manière suivante:

1
% npm install grunt-contrib-watch --save-dev

Et il se configure dans le Gruntfile suivant l’exemple ci-dessous :

Gruntfile.js
1
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
module.exports = function(grunt) {
  'use strict';

  // Project configuration.
  grunt.initConfig({

    dirs: {
      app:     'app/',
      js:      'app/js/',
      css:     'app/css/',
      public:  'public/'
    },

    files: {
      all: '**/*',
      js:  '**/*.js',
      css: '**/*.css',
      img: '**/*.{png,gif,jpg,jpeg}'
    },

    watch: {
      jshint: {
        files: ['<%= dirs.js %><%= files.js %>', '<%= dirs.public %><%= files.js %>', 'Gruntfile.js'],
        tasks: 'jshint'
      }
    }

  });


  // Load the plugin that provides the "watch" task.
  grunt.loadNpmTasks('grunt-contrib-watch');

  // Default task(s).
  grunt.registerTask('default', ['watch:jshint']);

};

On en profite pour ajouter 2 hash qui permettront ensuite de gagner du temps: dirs et files et qui définissent les répertoires et catégories de fichiers qui seront utilisés ensuite dans les tâches. Vous retrouvez les 2 lignes qui chargent le plugin (grunt.loadNpmTasks(‘grunt-contrib-watch’);) et enregistrent votre tâche (grunt.registerTask(‘default’, [‘watch:jshint’]);)

La configuration du pluging comporte une section qui défini la tâche à lancer jshint et quels sont les fichiers à surveiller. Dans notre cas, nous allons linter tous les fichiers js qui se trouvent dans les répertoires js et public, ainsi que le fichier Gruntfile.js.

Il ne reste plus qu’à exécuter la tâche :

1
2
3
% grunt watch:jshint
Running "watch:jshint" (watch) task
Waiting...

Encore un petit truc, si vous n’avez pas enregistré de tâche personnelles avec la propriété registerTask, vous pouvez toujours exécuter les tâches définies dans le initConfig().

Il ne nous reste plus qu’à provoquer un changement dans le fichier Gruntfile.js en supprimant un point-virgule en fin de ligne par exemple (ne pas oublier de sauver le fichier)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
% grunt watch:jshint
Running "watch:jshint" (watch) task
Waiting...

Reloading watch config...

Running "watch:jshint" (watch) task
Waiting...
>> File "Gruntfile.js" changed.
Running "jshint:files" (jshint) task

   Gruntfile.js
     47 |  })
             ^ Missing semicolon.

>> 1 error in 1 file
Warning: Task "jshint:files" failed. Use --force to continue.

Aborted due to warnings.
Completed in 0.946s at Sun Jul 06 2014 10:01:10 GMT+0200 (CEST) - Waiting...

Si on corrige l’erreur:

1
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
% grunt watch:jshint
Running "watch:jshint" (watch) task
Waiting...

Reloading watch config...

Running "watch:jshint" (watch) task
Waiting...
>> File "Gruntfile.js" changed.
Running "jshint:files" (jshint) task

   Gruntfile.js
     47 |  })
             ^ Missing semicolon.

>> 1 error in 1 file
Warning: Task "jshint:files" failed. Use --force to continue.

Aborted due to warnings.
Completed in 0.946s at Sun Jul 06 2014 10:01:10 GMT+0200 (CEST) - Waiting...

Reloading watch config...

Running "watch:jshint" (watch) task
Waiting...
>> File "Gruntfile.js" changed.
Running "jshint:files" (jshint) task
>> 1 file lint free.

Done, without errors.
Completed in 1.095s at Sun Jul 06 2014 10:02:58 GMT+0200 (CEST) - Waiting...

Ouf, la catastrophe est évitée.

grunt-shell

Le dernier plugin que nous allons étudier est le plugin grunt-shell. Ce plugin permet d’exécuter des commandes système dans grunt.

Dans notre exemple, nous allons créer une tâche qui réalise une commande git log. On pourra plus tard, réaliser un “rsync” pour déployer son application sur un serveur de préproduction.

Vous connaissez la manipulation, on commence par installer le plugin:

1
% npm install grunt-shell --save-dev

Et on le configure dans le Gruntfile suivant l’exemple ci-dessous :

Gruntfile.js
1
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
41
42
43
44
45
46
47
48
49
50
51
52
module.exports = function(grunt) {
  'use strict';


  // Shell logging function
  function logShell(err, stdout, stderr, cb) {
    if (err) {
      grunt.log.error('Command failed on ' + new Date());
    } else {
      grunt.log.ok('Command executed on ' + new Date());
    }
    cb();
  }

  // Project configuration.
  grunt.initConfig({

    dirs: {
      app:     'app/',
      js:      'app/js/',
      css:     'app/css/',
      public:  'public/'
    },

    files: {
      all: '**/*',
      js:  '**/*.js',
      css: '**/*.css',
      img: '**/*.{png,gif,jpg,jpeg}'
    },

    shell: {
      options: {
        failOnError: true,
        callback: logShell,
        stdout: true,
        stderr: true
      },
      gitlog: {
        command: 'git log'
      }
    }
  });


  // Load the plugin that provides the "shell" task.
  grunt.loadNpmTasks('grunt-shell');

  // Default task(s).
  grunt.registerTask('default', ['shell:gitlog']);

};

La section de configuration de la tâche shell contient 2 sous-sections. La première configure de manière générale le plugin. On remarque qu’il est possible d’appeler une callback (nous avons défini une fonction logShell) et d’activer les sorties standards stdout et stderr. Les sous-sections suivantes définissent les appels systèmes.

Essayons :

1
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
% grunt shell:gitlog
Running "shell:gitlog" (shell) task
commit 8a797952e14d0e5301432fce2dca8d944e172f36
Author: Jean-Thierry BONHOMME <jtbonhomme@gmail.com>
Date:   Sat Jul 5 17:04:32 2014 +0200

    Added jshint task

commit 85fc208f8e2ee257a782d0787c4233d3167044a8
Author: Jean-Thierry BONHOMME <jtbonhomme@gmail.com>
Date:   Sat Jul 5 16:18:21 2014 +0200

    Update article reference)

commit 752d0c8b6cc74c6a98417d43e42ba486020aa4db
Author: Jean-Thierry BONHOMME <jtbonhomme@gmail.com>
Date:   Sat Jul 5 16:17:34 2014 +0200

    First commit

commit 4b6649ed0c838723565649c6fbc0189bec19c874
Author: Jean-Thierry BONHOMME <jtbonhomme@gmail.com>
Date:   Sat Jul 5 16:14:15 2014 +0200

    Initial commit
>> Command executed on Sun Jul 06 2014 10:22:28 GMT+0200 (CEST)

Done, without errors.

On est bien d’accord que c’est juste un exemple, et que cette tâche ne présente aucun intérêt pratique, il serait évidemment plus simple d’entrer la commande % git log

Créer ses propres tâches

La documentation grunt détaille la manière de créer ses propres tâches.

Tâches personnalisées simples

Le plus simple pour créer une tâche est de la développer directement dans le Gruntfile :

1
2
3
grunt.registerTask('foo', 'My "foo" task.', function() {
  grunt.log.writeln('Execution de la tache "foo"');
});

Puis appelez la directement :

1
2
3
4
5
% grunt foo
Running "foo" task
Execution de la tache "foo"

Done, without errors.

C’est relativement pratique pour les scripts simple. Pour développer des tâches plus complexes, voici comment procéder.

Tâches personnalisées complexes

Les tâches personnalisées sont stockées dans un répertoire qui peut être chargé par grunt. Dans notre cas, nosu avons créé une tâche hash (merci @jinroh) qui calcul un hash md5 sur des fichiers.

On modifie les Gruntfile comme suit:

Gruntfile.js
1
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
41
module.exports = function(grunt) {
  'use strict';

  grunt.loadTasks('tasks');

  // Project configuration.
  grunt.initConfig({

    // custom variables configuration
    dirs: {
      app:     'app/',
      js:      'app/js/',
      css:     'app/css/',
      public:  'public/'
    },

    files: {
      all: '**/*',
      js:  '**/*.js',
      css: '**/*.css',
      img: '**/*.{png,gif,jpg,jpeg}'
    },

    hash: {
      options: {
        prefix: './'
      },
      publics: {
        src: [
          'Gruntfile.js'
          ],
        dest: '<%= dirs.public %>hash'
      }
    }

  });

  // Default task(s).
  grunt.registerTask('default', ['watch']);

};

Puis on crée un répertoire tasks contenant un fichiers hash.js:

tasks/hash.js
1
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
/* jshint node:true */

var crypto = require('crypto');

module.exports = function(grunt) {
  'use strict';

  function md5(file) {
    var data = grunt.file.read(file, { encoding: null });
    return crypto.createHash('md5').update(data).digest('hex');
  }

  grunt.registerMultiTask('hash', 'Create the hash file', function() {
    var prefix = new RegExp('^' + this.options().prefix);

    var hashes = [];
    var maxlen = 0;

    this.files.forEach(function(files) {
      files.src.map(function(file) {
        var name = file.replace(prefix, '') + ':';
        maxlen = Math.max(maxlen, name.length);
        return [name, md5(file)];
      }).forEach(function(data) {
        hashes.push(grunt.log.table([maxlen + 4, 32], data));
      });
    });

    hashes = hashes.join('\n') + '\n';

    grunt.log.writeln(hashes);
    grunt.log.write('Generating ' + this.data.dest.cyan + '...');
    grunt.file.write(this.data.dest, hashes);
    grunt.log.ok();
  });

};

Il ne reste plus qu’à lancer la tâche :

1
2
3
4
5
6
7
% grunt hash
Running "hash:publics" (hash) task
Gruntfile.js:    7d0428cb4f97db4bed4ce04e21eac969

Generating public/hash...OK

Done, without errors.

Conditions d’échec

Il faut noter que lorsqu’on défini une tâche comme une suite d’autres tâches, toute erreur sur une de ces tâches provoque l’arrêt de la tâche ‘parente’.

Par exemple l’exécution de la suite de tâche ci-dessous va échouer lors de la 2ème tâche (ko2) sans exécuter la 3ème (ok3) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
grunt.registerTask('ok1', 'This task succeds', function() {
  grunt.log.writeln('Execution de la tache "ok1"');
  return true;
});

grunt.registerTask('ko2', 'This task fails', function() {
  grunt.log.writeln('Execution de la tache "ko2"');
  return false;
});

grunt.registerTask('ok3', 'This task succeds', function() {
  grunt.log.writeln('Execution de la tache "ok3"');
  return true;
});

grunt.registerTask('ok-ko', ['ok1', 'ko2', 'ok3']);
1
2
3
4
5
6
7
8
9
% grunt ok-ko
Running "ok1" task
Execution de la tache "ok1"

Running "ko2" task
Execution de la tache "ko2"
Warning: Task "ko2" failed. Use --force to continue.

Aborted due to warnings.

Liens pour aller (encore) plus loin

Conclusion

Je vous conseille de jeter un oeil aux plugins qui permettent d’intégrer les framework de test dans grunt (grunt-mocha par exemple) et qui permettent d’aller très loin dans l’automatisation des tâches (corvées ?) courantes. Vous pouvez aussi examiner le module grunt-init qui permet d’initialiser des projets rapidement suivant des TEMPLATES prédéterminés.

La communauté est très active autour de ce projet, vous trouverez surement le plugin qu’il vous faut !

Enfin, pour supporter Grunt, vous pouvez d’ailleurs ajouter aux README.md de vos propres projets, la ligne

1
[![Built with Grunt](https://cdn.gruntjs.com/builtwith.png)](http://gruntjs.com/)

Ce qui fera apparaître le badge suivant :

Built with Grunt

Grunt: L’automatisation Facile (Part 1)

La majorité des développeurs que je connais ont un point commun, une caractéristique qui les distinguent des autres professions.

Ils sont paresseux.

Loin d’être un défaut, cette paresse est encouragée car elle se traduit par une tendance à éviter les tâches répétitives et fasitidieuses, et libèrent du temps pour les activités plus productives (écriture de documentation et test … quoique …)

Les outils qui permettent au développeur consciencieux d’en faire le moins possible foisonnent; générateurs de templates et boilerplates, snippets, gist et autres pastebin, et outils d’automatisation.

C’est à cette dernière catégorie d’outils que nous allons nous intéresser et plus particulièrement au plus célèbre d’entre eux: grunt.

tous les codes source de cet article sont disponibles sous github

A quoi ça sert ?

Grunt est un exécuteur de tâches qui peuvent être lancées unitairement, ou par groupes.

Pour parler concrêtement, le travail quotidien d’un dev front consiste quasi continuellement à :

  • développer ou modifier son code (ou celui du voisin)
  • corriger les fautes de syntaxes qui ne correspondent pas aux règles de codage
  • vider les répertoires de sortie où sont compilés ses fichiers
  • compiler ses templates pour générer les fichiers html
  • compiler et minifier ses fichiers html
  • lancer ses tests unitaires (si si si, il paraît que ça se fait beaucoup)
  • déployer son application
  • rafraîchir son navigateur

Et recommencer …

Grunt permet d’automatiser la plupart de ces tâches (non, il n’écrira pas le code ou la doc pour vous, mais il peut néanmoins générer la documentation yuidoc par exemple)

Si on ajoute à ça une communauté active qui propose un grand nombre de plugins pour vous faciliter la vie, pourquoi hésiter plus longtemps ?

Par quoi on commence ?

PS: toutes les manipulations ci-dessous tournent sous MAC, avec nodejs et npm installés

Il faut d’abord installer grunt et l’outil de ligne de commande grunt-cli

1
% npm install -g grunt-cli

Si à cette étape vous vous amusez à exécuter la commande grunt (ben, faites le, histoire de suivre un peu), vous risquez de voir ceci apparaître :

1
2
3
4
5
6
7
8
9
10
% grunt
grunt-cli: The grunt command line interface. (v0.1.9)

Fatal error: Unable to find local grunt.

If you're seeing this message, either a Gruntfile wasn't found or grunt
hasn't been installed locally to your project. For more information about
installing and configuring grunt, please see the Getting Started guide:

http://gruntjs.com/getting-started

Ce message indique que le moteur de grunt n’est pas installé, et que grunt utilise un fichier de configuration Grunfile.js décrivant l’ensemble des tâches que vous souhaitez le voir faire exécuter.

Configuration de grunt

Créez puis ouvrez les fichier suivant avec votre éditeur favori.

1
% touch Gruntfile.js package.json

Nous allons tout d’abord installer le package node de grunt en utilisant le gestionnaire de packages de node. Commencez par éditer le fichier package.json.

package.json
1
2
3
4
5
6
7
8
9
10
11
12
{
  "name": "grunt-article",
  "private": false,
  "description": "Grunt article source files",
  "version": "0.0.1",
  "author": {
    "name": "jtbonhomme"
  },
  "devDependencies": {
    "grunt" : "~0.4.5"
  }
}

Sauvez et lancez la commande qui installe les dépendances node:

1
npm install

A cette étape, vous devez pouvoir exécuter grunt (l’option -v pour verbose):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
% grunt -v
Initializing
Command-line options: --verbose

Reading "Gruntfile.js" Gruntfile...OK

Registering Gruntfile tasks.
Loading "Gruntfile.js" tasks...OK
>> No tasks were registered or unregistered.

No tasks specified, running default tasks.
Running tasks: default
Warning: Task "default" not found. Use --force to continue.

Aborted due to warnings.

On voit que grunt cherche dans le fichier Gruntfile.js les tâches à enregistrer et, en l’absence d’indication complémentaire, essaie de lancer une tâche default, qu’il ne trouve pas parce que, pour ceux qui suivent, nous n’avons encore rien écrit dans Gruntfile.js.

La première tâche

Imaginons que nous compilons notre projet et que le résultat soit déposé dans un répertoire public et que nous souhaitons définir une tâche qui permette de vider le contenu de ce répertoire.

Nous allons utiliser un des nombreux plugins de grunt : grunt-contrib-clean

1
% npm install grunt-contrib-clean --save-dev

l’option —save-dev permet d’ajouter automatiquement la dépendance au fichier package.json

Editez le fichier Gruntfile.js pour ajouter le code suivant:

Gruntfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = function(grunt) {
  'use strict';

  // Project configuration.
  grunt.initConfig({

    clean: {
      all: [
        'public'
      ]
    }
  });


  // Load the plugin that provides the "uglify" task.
  grunt.loadNpmTasks('grunt-contrib-clean');

  // Default task(s).
  grunt.registerTask('default', ['clean:all']);

};

Puis, créez un répertoire public, ajoutez-y un fichier test et lancez grunt:

1
2
3
4
5
6
7
8
9
10
11
% mkdir public
% touch public/test
% ls
Gruntfile.js node_modules package.json public
% grunt
Running "clean:all" (clean) task
Cleaning public...OK

Done, without errors.
% ls
Gruntfile.js node_modules package.json

Le répertoire public et son contenu ont bien été supprimés.

Nous avons créé notre première tâche grunt.

Nous verrons dans le prochain article comment utiliser les plugins et créer nos propres tâches.