Grunt, Bower, LESS & Bootstrap

Dans mon dernier billet, j'avais évoqué mon engouement pour Bower, une solution de gestion de dépendances web. J'avais alors regretté le fait de ne pas pouvoir exécuter des tâches post-installation afin de retravailler la version distribuée de Bootstrap pour l’alléger (comme on peut le faire ici). Comme j'en avais émis l'idée dans ce même billet, j'ai entrepris de ne plus utiliser Bower directement mais de l’utiliser au travers de Grunt.

grunt-bower.png

Si vous ne connaissez pas encore Grunt, il s'agit d'un ordonnanceur au même titre que Maven, ant, ou encore Phing, mais basé sur nodejs et utilisant une syntaxe JavaScript.

Installation de Bower & Grunt sous Fedora 20

Après avoir installé nodejs et npm via yum, il faudra installer Bower et Grunt avec npm. En effet, Bower est absent des dépôts Fedora et Grunt est bien disponible mais sans grunt-cli, ce qui en enlève tout l’intérêt...

sudo yum install npm -y
sudo npm install bower grunt grunt-cli -g

Notez au passage le fait que l'on fasse l'installation avec l'option -g pour avoir Grunt et Bower installés au niveau du système (/usr/lib/node_modules). Les autres installations se feront toujours à l'aide de npm mais sans cette option. Ceci permet de tout avoir à la racine du projet web, dans un répertoire node_modules (qu'il faudra exclure de la gestion de sources).

Voila, maintenant on rajoute un fichier packages.json et on installe les dépendances avec un npm install :

{
  "name": "Boldy",
  "version": "1.1.2",
  "description": "The Boldy theme for Dotclear",
  "repository": {
    "type": "git",
  },
  "author": "Guillaume Kulakowski",
  "homepage": "http://www.llaumgui.com",
  "devDependencies": {
    "bower": "latest",
    "grunt": "~0.4.2",
    "grunt-contrib-uglify": "latest",
    "grunt-contrib-concat": "latest",
    "grunt-contrib-less": "latest",
    "grunt-bower-task": "latest",
    "grunt-contrib-watch": "latest"
    "grunt-contrib-cssmin": "latest"
  }
}

J'ai maintenant un environnement de travail avec Bower et Grunt ainsi que les plugins nécessaires à mon projet.

Le fichier Gruntfile.js

La configuration des tâches Grunt passe par le fichier Gruntfile.js, j'ai mis le mien et vais en détailler les tâches :

module.exports = function(grunt) {
  'use strict';
 
  // Force use of Unix newlines
  grunt.util.linefeed = '\n';
 
  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    bowerrc: grunt.file.readJSON('.bowerrc'),
 
    // Path configuration from Gruntfile.js
    dirs: {
      'vendor': '<%= bowerrc.directory %>',
      'bootstrap': {
        'js': '<%= dirs.vendor %>/bootstrap/js',
        'less': '<%= dirs.vendor %>/bootstrap/less'
      },
      'css': 'css',
      'less': 'less',
      'js': 'js'
    },
 
    src: {
      output: {
        'bootstrap': {
          'js': '<%= dirs.vendor %>/bootstrap/my/js/bootstrap.js',
          'css': '<%= dirs.vendor %>/bootstrap/my/css/bootstrap.css'
        },
        'boldy': {
          css: {
            'screen': '<%= dirs.css %>/screen.css',
            'indefero': '<%= dirs.css %>/indefero.css'
          },
          js: '<%= dirs.js %>/global.js'
        }
      },
 
      input: {
        bootstrap: {
          'js': [
            '<%= dirs.bootstrap.js %>/dropdown.js',
            '<%= dirs.bootstrap.js %>/tooltip.js',
            '<%= dirs.bootstrap.js %>/collapse.js',
            '<%= dirs.bootstrap.js %>/transition.js',
            '<%= dirs.bootstrap.js %>/carousel.js'
          ],
          'less': [
            '<%= dirs.less %>/bootstrap.less'
          ]
        },
        indefero: '<%= dirs.css %>/indefero.src.css',
        less: '<%= dirs.less %>/boldy_boot.less',
        js: [
          '<%= dirs.vendor %>/jquery-cookie/jquery.cookie.js',
          '<%= src.output.bootstrap.js %>',
          '<%= dirs.vendor %>/scroll-to-top/jquery.scrollToTop.min.js',
          '<%= dirs.js %>/js/post.js',
          '<%= dirs.vendor %>/async-gravatars/async-gravatars.js',
          '<%= dirs.vendor %>/jquery-colorbox/jquery.colorbox-min.js',
          '<%= dirs.vendor %>/nwxforms/src/nwxforms.js',
          '<%= dirs.js %>/boldy.js'
        ]
      },
    },
 
    // Banner
    banner: '/*!\n' +
              ' * Boldy for Dotclear v<%= pkg.version %>\n' +
              ' * Original theme by Site5 (http://www.s5themes.com)\n' +
              ' * Under a GPLv2 http://www.gnu.org/licenses/gpl-2.0.txt\n' +
              ' * Ported on Dotclear by <%= pkg.author %> (<%= pkg.homepage %>)\n' +
              ' * Copyright 2012-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' +
              ' */',
 
 
 
    /********************************** Task **********************************/
    // Bower
    bower: {
      install: {
        options: {
          targetDir: '<%= dirs.vendor %>',
          cleanTargetDir: true,
          layout: 'byComponent',
          install: true,
          copy: false,
          verbose: true
        }
      }
    },
 
 
    // Concatenation
    concat: {
      bootstrap: {
        src: '<%= src.input.bootstrap.js %>',
        dest: '<%= src.output.bootstrap.js %>'
      },
    },
 
 
    // LESS
    less: {
      boldy: {
        options: {
          compress: true,
          cleancss: true,
          report: 'gzip',
          strictImports: true,
        },
        files: { '<%= src.output.boldy.css.screen %>': '<%= src.input.less %>'},
      },
      boldy_debug: {
          options: {
            compress: false,
            cleancss: false,
            report: 'none',
            strictImports: true,
          },
          files: { '<%= src.output.boldy.css.screen %>': '<%= src.input.less %>'},
      },
      bootstrap: {
        files: { '<%= src.output.bootstrap.css %>': '<%= src.input.bootstrap.less %>'}
      }
    },
 
 
    // Watcher
    watch: {
      boldy: {
        files: ['less/*.less'],
        tasks: ['less:boldy_debug'],
        options: {
          spawn: false,
        },
      },
    },
 
 
    // CSSmin
    cssmin: {
      boldy: {
        options: {
          report: 'gzip',
          banner: '<%= banner %>'
        },
        files: {
          '<%= src.output.boldy.css.indefero %>': '<%= src.input.indefero %>'
        }
      }
    },
 
 
    // Uglify
    uglify: {
      boldy: {
        options: {
          report: 'gzip',
          banner: '<%= banner %>'
        },
        files: {
          '<%= src.output.boldy.js %>': '<%= src.input.js %>',
        }
      }
    }
 
  });
 
 
  // Load plugins
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-less');
  grunt.loadNpmTasks('grunt-bower-task');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-contrib-cssmin');
 
 
  // Callable tasks
  grunt.registerTask('default', ['boldy', 'cssmin', 'concat:bootstrap', 'uglify']);
  grunt.registerTask('w', ['watch:boldy']);
 
  grunt.registerTask('bootstrap', ['concat:bootstrap', 'less:bootstrap']);
  grunt.registerTask('boldy', ['less:boldy']);
 
  grunt.registerTask('full', ['bower', 'bootstrap', 'default']);
  grunt.registerTask('all', ['full']); // Alias
};

Tâche bower

Via le plugin grunt-bower-task.

Cette tâche est là pour exécuter Bower au travers de Grunt, notez que le plugin n'interprète pas le .bowerrc, je l'ai donc chargé moi même (bowerrc: grunt.file.readJSON('.bowerrc')) afin de passer les mêmes options à la tâche Grunt qu'à Bower.

Tâche Bootstrap

Via les plugins grunt-contrib-less & grunt-contrib-concat.

Le but de cette tâche est de ne pas utiliser la version distribuée de Bootstrap mais de reconstruire ma propre version. Pour celà,

  • je reconstruis le JavaScript à partir des sources et du plugin concat,
  • je recompile les CSS à partir d'une version allégée du bootstrap.less :
/* -- BEGIN LICENSE BLOCK ---------------------------------------
# This file is part of Boldy, a theme for Dotclear
#
# Theme by Site5 (http://www.s5themes.com)
# under a GPLv2 http://www.gnu.org/licenses/gpl-2.0.txt
# Ported on Dotclear by Guillaume Kulakowski (http://www.llaumgui.com)
#
# -- END LICENSE BLOCK ----------------------------------------- */
 
// Core variables and mixins
@import "../vendor/bootstrap/less/variables.less";
@import "../vendor/bootstrap/less/mixins.less";
 
// My override
@import "bootstrap_variables.less";
//@import "bootstrap_mixins.less";
 
// Reset
@import "../vendor/bootstrap/less/normalize.less";
@import "../vendor/bootstrap/less/print.less";
 
// Core CSS
@import "../vendor/bootstrap/less/scaffolding.less";
@import "../vendor/bootstrap/less/type.less";
@import "../vendor/bootstrap/less/code.less";
@import "../vendor/bootstrap/less/grid.less";
//@import "../vendor/bootstrap/less/tables.less";
@import "../vendor/bootstrap/less/forms.less";
@import "../vendor/bootstrap/less/buttons.less";
 
// Components
@import "../vendor/bootstrap/less/component-animations.less";
//@import "../vendor/bootstrap/less/glyphicons.less";
@import "../vendor/bootstrap/less/dropdowns.less";
//@import "../vendor/bootstrap/less/button-groups.less";
//@import "../vendor/bootstrap/less/input-groups.less";
@import "../vendor/bootstrap/less/navs.less";
@import "../vendor/bootstrap/less/navbar.less";
//@import "../vendor/bootstrap/less/breadcrumbs.less";
@import "../vendor/bootstrap/less/pagination.less";
@import "../vendor/bootstrap/less/pager.less";
//@import "../vendor/bootstrap/less/labels.less";
//@import "../vendor/bootstrap/less/badges.less";
//@import "../vendor/bootstrap/less/jumbotron.less";
@import "../vendor/bootstrap/less/thumbnails.less";
@import "../vendor/bootstrap/less/alerts.less";
//@import "../vendor/bootstrap/less/progress-bars.less";
//@import "../vendor/bootstrap/less/media.less";
@import "../vendor/bootstrap/less/list-group.less";
@import "../vendor/bootstrap/less/panels.less";
@import "../vendor/bootstrap/less/wells.less";
//@import "../vendor/bootstrap/less/close.less";
 
// Components w/ JavaScript
//@import "../vendor/bootstrap/less/modals.less";
@import "../vendor/bootstrap/less/tooltip.less";
//@import "../vendor/bootstrap/less/popovers.less";
@import "../vendor/bootstrap/less/carousel.less";
 
// Utility classes
@import "../vendor/bootstrap/less/utilities.less";
@import "../vendor/bootstrap/less/responsive-utilities.less";

Remarquez que j'utilise mes propres variables après celles de Bootstrap afin de les écraser et de modifier Bootstrap directement à la source.

Tâche Boldy

Via les plugins grunt-contrib-uglify, grunt-contrib-less & grunt-contrib-cssmin.

Comme en recompilant Bootstrap à partir des sources LESS j'ai adhéré au concept de LESS, j'ai réécrit le thème de mon blog en LESS.

Cette tâche boldy sert donc :

  • à générer le CSS minifié à partir des sources LESS (grunt-contrib-less),
  • à minifier la CSS pour mon thème indefero qui reste en CSS (grunt-contrib-cssmin),
  • et à minifier les Javascripts (grunt-contrib-uglify).

Tâche watch

Via le plugin grunt-contrib-watch.

Pour compiler du LESS en CSS, il y a 2 méthodes.

  • soit passer par le less.js qui va parser en JS les fichiers LESS à chaque chargement de page (2 à 3 secondes pour mon blog),
  • soit passer par une tâche watch dans Grunt, qui va écouter les modifications sur les fichiers LESS et recompiler un CSS.

C'est cette dernière solution que j'ai retenu et donc c'est à cela que sert la tâche watch.

En conclusion

Pour conclure, je dirais que la mise en place de ces outils (surtout pour la première fois) est certes chronophage, mais une fois que ça tourne, pour mettre à jour les libs (Bootstrap, jQuery, etc.), une seule ligne de commande suffit (grunt full). De plus, une fois Grunt et Bower en place (et maîtrisés), passer de CSS à LESS a été super simple.

Pour finir, je regrette presque que nodejs n'ait pas été inclus dans Fedora plus tôt car il propose de formidables outils indispensables aux développeurs (front) web.

Et pour voir ce que ça donne, une petite vidéo:

Attribution - Partage dans les Mêmes Conditions 4.0 International