Pourquoi je préfère Brunch

Ah, vous croyez que vous êtes au top parce que vous utilisez Grunt, Gulp, Broccoli ou même Glou ? Eh ben non, pas forcément, voire carrément pas. Du tout. Voilà déjà 4 ans que Brunch existe, et sur bien des scenarii courants, il bat tout ce petit monde à plate couture.

Envie d’en savoir plus, et de voir plein d’exemples ? C’est par ici.

UPDATE: This article has been translated to English and adapted to become the official Brunch Guide (EN/FR). Only the Guide will be maintained and updated looking forward.

Brunch ?! C’est quoi, Brunch ?

Brunch est un builder. Pas un exécuteur de tâches générique, mais un outil spécialisé dans la production de fichiers finaux pour la production, à partir de tout un tas de fichiers de développement.

Ce type de besoin est extrêmement fréquent chez les devs front et les designers front, qui ont souvent besoin de faire un peu les mêmes choses : partir d’une arborescence de fichiers LESS ou SASS pour produire un ou plusieurs CSS minifiés, pareil pour du JS, des images et leurs sprites, etc.

Brunch face aux autres

L’immense majorité des personnes qui automatisent ce type de tâches utilisent soit Grunt, soit Gulp (et parfois Broccoli ou Glou). Bien qu’extrêmement populaires, et arrivés sur le marché plus récemment que Brunch, ces solutions lui sont souvent inférieures dans les scenarii d’utilisation courants.

J’utilise Brunch depuis juin 2012 (autour de sa version 1.3 ; il remonte au printemps 2011), et jusqu’à présent, il constitue encore pour moi une alternative très supérieure aux autres acteurs apparus depuis.

Afin de bien cerner ce qui distingue Brunch des autres solutions du marché, nous allons aborder différents aspects techniques et architecturaux, qui constituent chacun des choix de conception autour desquels se répartit l’écosystème.

Après quoi, on se tapera tout plein de démos sur du code concret, n’ayez crainte.

Exécuteurs de tâches vs. outils de build

Le marché est dominé par des exécuteurs de tâches génériques. Ces outils fournissent un mécanisme de description de tâches, et de dépendances entre ces tâches. Il peut s’agir de n’importe quoi : copier un fichier, en produire un, envoyer un e-mail, compiler quelque chose, lancer les tests, faire un commit Git… absolument ce que vous voulez.

C’est une notion très ancienne ; un des premiers exécuteurs de tâche génériques connus était Make (et ses fameux fichiers Makefile) ; dans l’univers Java, on a d’abord eu Ant, et comme si ça n’était pas assez verbeux, on a désormais l’énorme mammouth sclérosé qu’est Maven ; Ruby de son côté a Rake, etc.

Parce que ces exécuteurs sont génériques, il ne leur est que rarement possible d’optimiser automatiquement pour des scenarii spécifiques, ou même de mettre en place des conventions par défaut. Toute tâche nécessite l’écriture d’un volume non trivial de code et/ou de configuration, et doit être invoquée explicitement aux bons endroits.

Qui plus est, toute tâche—et même tout comportement de fond, comme la surveillance des fichiers pour mettre à jour le build—nécessite l’écriture d’un plugin, son chargement, sa configuration, etc.

Call To Action Item Title

Call to cation item caption

Adapter Brunch à un projet existant

Supposons à présent que vous partiez d’un projet existant, dont vous souhaitez confier le buildfront à Brunch. Peut-être venez-vous de Grunt, ou Gulp, ou que sais-je… Peu importe. Il faut se poser quelques questions de base :

  1. Où sont les fichiers sources pour le build ?
  2. Quels langages utilisent-ils ?
  3. Dans quel dossier cible va le build ?
  4. Quel est le mapping source -> cible au sein du build ?
  5. Est-ce que je veux enrober mon JS applicatif en modules ?

Le point 1 détermine la valeur du réglage paths.watched, pour le ou les répertoires de base à exploiter/surveiller. La valeur par défaut est ['app', 'test', 'vendor'], mais il y a fort à parier que vous devrez changer ça. Autres réglages concernés : conventions.assets, qui va déterminer les dossiers dont le contenu sera copié-collé tel quel, et conventions.vendor, qui indique les dossiers dont le JS ne doit pas être enrobé en modules, s’il y en a (attention, si vous passez par Bower, les composants qu’il fournit ne sont jamais enrobés).

Le point 2 détermine les plugins Brunch à utiliser, sachant qu’on peut très bien mélanger les genres ; par exemple, quand j’utilise Bootstrap, je préfère largement utiliser son code source SASS, afin de facilement personnaliser le thème, notamment dans _variables.scss. Cependant, je préfère Stylus pour mes propres styles, j’ai donc souvent à la fois sass-brunch et stylus-brunch installés.

Si mon app utilise du MVC côté client, je vais toujours isoler mes templates dans leurs propres fichiers, et donc utiliser par exemple jade-brunch ou dust-linkedin-brunch pour les convertir de façon transparente en modules exportant une unique fonction de rendering qui résulte de la précompilation du template.

Le point 3 détermine la valeur du réglage paths.public, qui vaut par défaut 'public'. Ce dossier n’a pas besoin d’exister en début de build. Les fichiers cibles sont exprimés relativement à ce chemin de base.

Le point 4 gouverne la structure du réglage files, avec jusqu’à trois sous-sections :

  • javascripts : tout ce qui produit du JS à terme, hors pré-compilation de templates (statut spécial) ;
  • stylesheets : tout ce qui produit du CSS à terme ;
  • templates : tout ce qui concerne la précompilation de templates pour produire à chaque fois une fonction de rendering (avec un argument contenant le presenter, ou view model, et le HTML en valeur de retour synchrone). Souvent, la cible sera la même que pour la partie noyau de javascripts.

Chacune de ces clés peut être très simple ou très avancée. Si on fournit juste un chemin de fichier (une String) comme valeur, tous les fichiers candidats iront vers cette unique concaténation. Si on fournit plutôt un objet, les clés sont les chemins cibles, et les valeurs, qui déterminent quelle portion de la codebase source va vers la cible, sont des ensembles anymatch, c’est-à-dire qu’il peut s’agir de :

  • Une simple String, qui devra correspondre au chemin exact du fichier, tel que perçu par Brunch (on y reviendra dans un instant) ;
  • Une expression rationnelle, qui devra correspondre au chemin du fichier ; très utile pour des préfixes, genre /^app\// ou /^vendor\// ;
  • Une fonction prédicat, qui prendra le chemin exact en argument et renverra de façon synchrone une valeur interprétée comme booléenne ;
  • Un tableau de valeurs, qui peuvent chacune être un des types précédents.

C’est donc extrêmement flexible comme méthode de répartition entre les cibles (ou même si on n’a qu’une cible, mais qu’on veut filtrer ce qui y va).

Le point 5 ne devrait pas être une vraie question : bien sûr que vous devriez utiliser des modules. On est en 2015, bordel, faut sortir de la préhistoire et du code bordélique, les gens ! Et tant qu’à faire, vous devriez opter pour CommonJS jusqu’à nouvel ordre (ce qui facilite le JS isomorphique et la migration ultérieure vers des modules natifs ES6).

Ça se définit avec les réglages modules.wrapper et modules.definition. Si vous avez décidé de rester en mode porcherie, vous pouvez les mettre tous les deux à false. Si vous croyez encore en AMD (ou que la Terre est plate), vous pouvez mettre amd. Il est même possible de fournir des fonctions de personnalisation pour des systèmes plus exotiques. Par défaut, les deux valent commonjs.

Un dernier point, si vous recourez aux modules (bravo !), est le nom de ces modules. Par défaut, il reprend le chemin exact vu par Brunch, moins le préfixe app/ si vous êtes restés sur les chemins sources conventionnels. Si vous avez plusieurs racines personnelles définies dans paths.watched, celles-ci restent comme préfixes. Dans tous les cas, l’extension du fichier dégage.

Si cela vous dérange, par exemple parce que vous voulez isoler les bibliothèques tierces dans app/externals/, et préserver leurs noms longs par-dessus le marché (genre jquery-1.11.2-min.js et moment-2.2.1-fr.js), mais voulez conserver des noms de modules simples ("jquery" et "moment"), vous devrez fournir une fonction de calcul de nom de module personnalisée, via le réglage modules.nameCleaner.

I sink under the weight

A wonderful serenity has taken possession of my entire soul, like these sweet mornings of spring which I enjoy with my whole heart. I am alone, and feel the charm of existence in this spot, which was created for the bliss of souls like mine. I am so happy, my dear friend, so absorbed in...
Read More

Leave a Reply