Accueil Nos publications Blog Grunt (3) : Configurer les tâches

Grunt (3) : Configurer les tâches

Grunt : The JavaScript Task Runner

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

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"));
});

grunt simple task configuration

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é
  });
}

grunt simple_task options

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

grunt simple_task override 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'

Templates

Un template, spécifié par les délimiteurs <%= %>, est évalué récursivement et automatiquement :

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

grunt multi_task

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.

  • filterstring 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.
  • nonullbool : Si true, conserve les motifs même s’il n’y a aucune correspondance.
  • dotbool : Si true, autorise les motifs à remonter les fichiers commençant par un ., même si le motif ne le permet pas explicitement.
  • matchBasebool : Si true, les motifs sans / fonctionnent comme s’ils étaient préfixés de **/
  • expandbool : 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 :

  • cwdstring : Toutes les correspondances de sources src sont relatives à ce chemin sans l’inclure.
  • srcstring array : Motif(s) de correspondance, relatifs à cwd.
  • deststring : Préfixe du chemin de destination.
  • extstring : Remplace toute extension existante par cette valeur dans les chemins de destination générés.
  • extDotstring : 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.
  • flattenbool : Supprime toute l’arborescence de destination dest générée.
  • renamefunction : 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 :

fichiers

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

grunt sample (files)

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 !