Autour du codeDevelopper toujours mieux
Posté le

Les gestionnaires de tâche: Grunt

Aujourd'hui on va parler de gestionnaire de tâche. Dans un projet, il y a souvent une multitude de tâches redondantes comme la compilation, le lancement des tests ou la création de fichier de configuration dédié à l'environnement de développement. Un gestionnaire de tâches permet d'avoir un seul point d'entrée pour exécuter toutes ces commandes et de gérer les dépendances entre elles.

Grunt est un outil qui s'exécute dans un environnement NodeJS donc la première chose et d'avoir cet environnement installé. Ensuite, il y a deux parties à installer le CLI (Command Line Interface) qui est globale à la machine et le gestionnaire de tâche lui-même qui est dépendant du projet. Cela permet d'avoir plusieurs projets avec des versions de Grunt différentes sur la même machine. L'installation du CLI se fait avec la commande suivante.

# npm install -g grunt-cli

Pour installer le gestionnaire de taches placez vous dans le répertoire de votre projet. Si votre projet ne contient pas de fichier package.json, on va en créer un. Cela va permettre par la suite à une personne qui récupère votre projet d'installer la même version de Grunt ainsi que les différents plugins grunt utilisés dans le projet. Le fichier package.json se créé via la commande:

$ npm init

Saisissez ensuite la commande suivante:

$ npm install grunt --save-dev

Le --save-dev permet de déclarer l'installation de Grunt dans le package.json. Maintenant que tout est en place, c'est le moment de déclarer nos tâches dans un fichier Gruntfile.js Ce fichier doit déclarer une fonction qui prend une instance de Grunt en paramètre.

module.exports = function(grunt) {

};

Lorsque l'on souhaite ajouter une tâche on utilise la fonction grunt.registerTask le premier paramètre est le nom de la tâche le deuxième est facultatif, c'est la description de la tâche et le dernier est la fonction à exécuter.

module.exports = function(grunt) {
    grunt.registerTask('hello', 'Print hello.', function() {
        grunt.log.writeln('Hello world');
    });
};

Si l'on veut exécuter la commande hello on tapera grunt hello dans un terminal.

$ grunt hello

Une des fonctions de Grunt utiles pour écrire vos propre tâches est grunt.util.spawn qui permet de faire des appels systèmes mais, vous risquez de ne plus être cross plate-form.

grunt.registerTask('ls', 'Display file listing', function() {
    var done = this.async();
    grunt.util.spawn({
            cmd: 'ls',
            args: ['-l']
        },
        function doneFunction(error, result, code) {
            if (error) {
                grunt.fail.warn(error, code);
            } else {
                grunt.log.ok(String(result));
            }
            done();
        }
    );
});

Il est d'usage de créer une tâche 'default' dans notre Gruntfile. La tâche default est accessible simplement en tapant grunt.

Dans notre exemple, elle va appeler la tâche 'hello' puis 'ls'

grunt.registerTask('default', ['hello', 'ls'])

Les plugins grunt, ne développez plus vos tâches, configurez-les !

Maintenant que nous avons vu le fonctionnement de Grunt, Nous allons voir ce qui fait ça force, le système de plugin. Dans la pratique vous aurez rarement à développer vos tâches comme on la fait ci-dessous, mais plus à installer des plugins et à les configurer. Pour exemple, on va prendre le plugin grunt copy qui permet de copier des fichier et répertoire. Dans Grunt, tous les plugins se configurent de la même façon pour ce qui est des entrées/sortie donc si vous savez configurer le plugin copy vous saurez configurer tous les plugins.

Commençons par installer le plugin copy:

$ npm install --only=dev grunt-contrib-copy

Maintenant il faut indiquer dans notre Gruntfile que l'on souhaite utiliser grunt copy:

grunt.loadNpmTasks('grunt-contrib-copy');

Puis on va le configurer avec la fonction grunt.initConfig qui prend en paramètre un objet contenant les configurations des plugins que l'on a ajoutés.

Chaque plugin peut avoir plusieurs configurations au sein d'un même Gruntfile c'est ce que l'on appelle des cibles. Par exemple vous pouvez avoir besoin du plugin copy lors de la distribution de votre projet mais également lors de la construction de la documentation. On va donc déclarer les cibles dist et doc de copy.

module.exports = function(grunt) {
    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.initConfig({
      copy: {
           dist: {

           },
           doc: {

           }
      }
    });
}

Au sein de ces cibles, nous allons indiquer les entrées/sorties en passant un tableau d'objet à la propriété files.

grunt.initConfig({
    copy: {
        dist: {
            files: [
                {src: ['src/a.css', 'src/b.css'], dest: 'dest/'},
            ]
        },
        doc: {

        }
   }
});

Dans l'exemple ci-dessus lorsque l'on va saisir la commande:

$ grunt copy:dist

Le plugin va utiliser les chemins src/a.css et src/b.css du répertoire courant en entrée et les destinations dest/src/a.css et dest/src/b.css. Le répertoire courant sera toujours le même que l'emplacement du Gruntfile.

Si ce n'est pas ce que vous souhaitez et que vous préférez les destinations dest/a.css et dest/b.css. Il y a plusieurs possibilités.

L'une consiste à utiliser le mode expand et de données un répertoire à partir duquel les sources seront lues via la propriété cwd:

files: [
    {expand: true, cwd: 'src',
     src: ['a.css', 'b.css'], dest: 'dest/'},
]

a et b sont cherchés dans le répertoire src et placés dans dest/

On peut également utiliser propriété flatten pour indiquer d'utiliser que le nom de fichier dans la génération du chemin de sortie.

files: [
    {expand: true, flatten: true,
     src: ['src/a.css', 'src/b.css'], dest: 'dest/'},
]

Changer l'extension du fichier.

On peut changer l'extension du fichier de sortie grâce à 'ext'

files: [
    {expand: true, flatten: true, ext: '.js',
     src: ['src/ts/a.ts', 'src/ts/b.ts'], dest: 'dest/js/'},
]

Renommer une partie du chemin

On peut utiliser une fonction passée à la propriété rename pour modifier le chemin. Par exemple si l'on souhaite que les fichiers suffixés par "-en" soit placés dans un sous-répertoire en et que les fichiers suffixés par "-fr" soit placés dans fr on peut écrire ceci, après avoir importé le module path.

files: [
    {expand: true,
     rename: function(dest, src) {
                 var srcObj = path.parse(src);
                 var langDir = srcObj.name.split('-')[1];
                 var fileName = srcObj.name.split('-')[0] + srcObj.ext;
                 return path.join(dest, langDir, fileName);
     },
     src: ['doc/a-fr.rst', 'doc/a-en.rst'], dest: 'dest/doc/'}
]

La syntaxe glob

Il est possible d'utiliser la syntaxe glob dans les chemins de fichier ainsi

src: ['doc/a-fr.rst', 'doc/a-en.rst']

peut s'écrire:

src: ['doc/\*.rst']

Ce qui évite d'avoir à rééditer le Gruntfile à chaque ajout de nouveau fichier.

Les raccourcis

Les raccourcis de syntaxe sont utilisables que dans certain cas.

Si votre plugin n'utilise n'a pas de sortie c'est par exemple le cas comme des analyseurs statiques (jslint, pylint...) ou qu'une seule sortie, vous pouvez utiliser les propriétés src et dest directement à la place de la propriété files.

uneCible: {
    src: ["fichier.1", "fichier.2"]
}

// ou

uneCible: {
    src: ["fichier.1", "fichier.2"],
    dest: "sortie"
}

Si vous n'utilisez pas le mode expand, vous pouvez utiliser un objet pour valeur de la propriété files:

files: {
    'dest/sortie.1': ['src/fichier.11', 'src/fichier.12'],
    'dest/sortie.2': ['src/fichier.21', 'src/fichier.22'],
}

Personnellement, je préfère ne pas utiliser les raccourcis, car le Gruntfile est plus fastidieux à relire et à modifier s'il mélange les trois types de syntaxe.

Voilà, j'espère que ça vous a plu, à vos IDE et bon code. laughing