Grunt, automatizando el trabajo repetitivo

13/01/2015
Grunt, automatizando el trabajo repetitivo

Grunt es un automatizador de tareas Javascript que nos permite lanzar una serie de tareas en bloque mediante una sola orden. Al estar basado en Javascript, tanto la sintaxis como el funcionamiento es muy sencillo, trabajando en base a plugins.

Instalación

npm install -g grunt-cli

Vamos a necesitar dos ficheros principales que colocaremos en la raiz del proyecto en el que Grunt realizará sus tareas:

package.json

{
  "name": "ExampleProject",
  "version": "0.1",
  "dependencies": {
   
  },
  "devDependencies": {

  }
}

y Gruntfile.js

module.exports = function(grunt){
  grunt.initConfig({

 
 });
}

Tal como los veis ahora es el estado básico de los dos ficheros. El fichero package.json es el responsable de las dependencias de paquetes para que npm nos ayude a gestionar esas dependencias y versiones de nuestra aplicación, de forma que ejecutando un simple 'npm install' en el directorio donde está este archivo, nos instalaría todos los paquetes referenciados en él (y sus dependencias secundarias).

Gruntfile.js es el fichero de configuración de Grunt. En él especificaremos los plugins, la configuración de cada uno de ellos y las tareas a ejecutar en cada caso. Dentro del grunt.config({ }) especificamos la configuración de las tareas; si estamos usando un plugin, este debe cargarse, fuera del grunt.config con la function grunt.loadNpmTasks( ); y las tareas se especifican con grunt.registerTask( ). Veámoslo con algunos ejemplos:

Plugins

Repasando las tareas repetitivas básicas que podemos encontrar en casi cualquier proyecto encontramos: compilación de Sass - Compass, compilación de CoffeeScript, compresión de Javascript y el CSS resultante, ejecutar algun comando (como por ejemplo 'drush cc all') y tantas otras.

grunt-contrib-compass

El plugin de compass es un obligatorio para nosotros para compilar y establecer la configuración de Compass en diferentes entornos. 

npm install grunt-contrib-compass --save-dev

Siempre que guardemos con --save-dev, nos guardará la dependencia en nuestro package.json él solo.

Dentro de nuestro Gruntfile.js, 

module.exports = function(grunt){
  grunt.initConfig({
    compass: {
        dist: {
          sassDir: ['sass'],
          cssDir: ['css'],
          environment: 'production'
        },
        dev: {
          sassDir: ['sass'],
          cssDir: ['css'],
          environment: 'development'
        },
    }
  });
  grunt.loadNpmTasks('grunt-contrib-compass');
  grunt.registerTask('dev', ['compass:dev']);
  grunt.registerTask('prod', ['compass:dist']);
}

Hemos establecido dos tareas con grunt.registerTask( ): dev y prod, una para cada entorno que hemos especificado en nuestra configuración un poco más arriba. 

  grunt.registerTask('dev', ['compass:dev']);
  grunt.registerTask('prod', ['compass:dist']);

Si queremos que compile Compass para desarrollo, sólo tendríamos que ejecutar desde terminal un 'grunt dev'; o 'grunt prod' para que ejecute la compilación de producción. Como ahora mismo solo tenemos las tareas correspondientes a grunt-contrib-compass, no podemos añadir más, pero habitualmente una tarea no sólo conllevará una acción, sino varias más, como vamos a ver a continuación.

 

grunt-contrib-coffee

Con CoffeeScript nos encontramos con la misma situación que con compass: podemos tener un proceso con un 'coffee -cwo' para compilar y quedarse a la espera de cambios, o podemos incluirlo en nuestro grunt y desentendernos de ello. Para instalarlo no hay más que ejecutar 

npm install grunt-contrib-coffee --save-dev

Es un plugin tremendamente completo con muchas opciones y que permite concatenar, expandir en ficheros, creación de Sourcemaps y demás.

...

coffee: {
  compile: {
    files: {
        // 1:1 compile
      'path/to/result.js': 'path/to/source.coffee', 
        // compile and concat into single file
      'path/to/another.js': ['path/to/sources/*.coffee', 'path/to/more/*.coffee'] 
    }
  },
  compileWithMaps: {
    options: {
      sourceMap: true
    },
    files: {
      'path/to/result.js': 'path/to/source.coffee', 
      'path/to/another.js': ['path/to/sources/*.coffee', 'path/to/more/*.coffee']
    }
  },
  glob_to_multiple: {
    expand: true,
    flatten: true,
    cwd: 'path/to',
    src: ['*.coffee'],
    dest: 'path/to/dest/',
    ext: '.js'
  }
}

...

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

Actualizamos así mismo nuestras tareas:

  grunt.registerTask('dev', ['compass:dev','coffee:compileWithMaps']);
  grunt.registerTask('prod', ['compass:dist','coffee:compile']);

 

Ahora cuando ejecutemos 'grunt dev', primero compilará Compass en desarrollo y luego Coffee con Sourcemaps. Pero, ¿qué ocurre cuando hacemos cambios?

 

grunt-contrib-watch

En un entorno de desarrollo siempre viene bien una compilación automática conforme vayamos haciendo cambios para no tener que pararnos continuamente a compilar y probar. Esto lo podemos hacer gracias a 'watch', algo que habitualmente tenemos en compass ('watch') pero que aquí extendemos a modificaciones de JS o CoffeeScript. 

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

En nuestra configuración especificamos qué ocurre cuando se ve un cambio; en nuestro caso vamos a diferenciar entre cambios de configuración, cambios en JS-Coffee, y cambios en los ficheros Sass (Compass).

...
watch: {
  configFiles: {
    files: [ 'Gruntfile.js', 'config/*.js' ],
    options: {
      reload: true
    }
  },
  css: {
    files: ['sass/**/**/**'],
    tasks: ['compass:dev'],
    options: {
        spawn: false
    }
  },
  js: {
    files: ['coffee/**/**/**'],
    tasks: ['coffee:compileWithMaps'],
    options: {
        spawn: false
    }
  }
},
...
grunt.loadNpmTasks('grunt-contrib-watch');
...
grunt.registerTask('dev', ['compass:dev','coffee:compileWithMaps','watch']);

De esta forma, Grunt vigila cambios en su propia configuración para recargarse en caso necesario y vigila cambios en coffee y sass. 

grunt-contrib-shell

Esto ya va para nota: ¿qué ocurre si queremos lanzar un comando en el terminal automáticamente con grunt? ¿Y si queremos que se lance cada vez que nuestro watch detecte un cambio? Para eso tenemos grunt-shell:

npm install grunt-shell --save-dev

En nuestro caso es bastante habitual ejecutar un 'drush cc all' o 'drush cc css-js' cada vez que realizas cambios en un portal en el que ya tienes activada la caché general de Drupal. Veamos como lo configuramos:

...
    shell: {
      'options': {
        'stdout': true,
        'stderr': true,
        'failOnError': true
      },
      drushall: {
        command: 'drush cc all',
      },
      drushcssjs: {
        command: 'drush cc css-js',
      }
    },
...

  grunt.loadNpmTasks('grunt-shell');

Hemos establecido dos configuraciones de tarea: una para limpiar la caché general, y otra para limpiar la de css-js del theme; la primera la ejecutamos al iniciar grunt, y la segunda la ejecutaremos cuando detectemos cambios con watch. 

Aquí tenéis el fichero resultante de todo esto: algo que cubre las áreas más habituales de un proyecto y que nos ayudará a desentendernos de esas tareas repetitivas tan tediosas. 

module.exports = function(grunt){
  grunt.initConfig({
    shell: {
      'options': {
        'stdout': true,
        'stderr': true,
        'failOnError': true
      },
      drushall: {
        command: 'drush cc all',
      },
      drushcssjs: {
        command: 'drush cc css-js',
      }
    },
    coffee: {
      compile: {
        files: {
          'path/to/result.js': 'path/to/source.coffee', 
          'path/to/another.js': ['path/to/sources/*.coffee', 'path/to/more/*.coffee'] 
        }
      },
      compileWithMaps: {
        options: {
          sourceMap: true
        },
        files: {
          'path/to/result.js': 'path/to/source.coffee', 
          'path/to/another.js': ['path/to/sources/*.coffee', 'path/to/more/*.coffee']
        }
      },
      glob_to_multiple: {
        expand: true,
        flatten: true,
        cwd: 'path/to',
        src: ['*.coffee'],
        dest: 'path/to/dest/',
        ext: '.js'
      }
    },
    compass: {
      dist: {
        sassDir: ['sass'],
        cssDir: ['css'],
        environment: 'production'
      },
      dev: {
        sassDir: ['sass'],
        cssDir: ['css'],
        environment: 'development'
      },
    },
    watch: {
      configFiles: {
        files: [ 'Gruntfile.js', 'config/*.js' ],
        options: {
          reload: true
        }
      },
      css: {
        files: ['sass/**/**/**'],
        tasks: ['compass:dev','shell:drushcssjs'],
        options: {
            spawn: false
        }
      },
      js: {
        files: ['coffee/**/**/**'],
        tasks: ['coffee:compileWithMaps','shell:drushcssjs'],
        options: {
            spawn: false
        }
      }
    }
  });
  grunt.loadNpmTasks('grunt-shell');
  grunt.loadNpmTasks('grunt-contrib-coffee');
  grunt.loadNpmTasks('grunt-contrib-compass');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.registerTask('dev', ['compass:dev','coffee:compileWithMaps','watch','shell:drushall']);
  grunt.registerTask('prod', ['compass:dist','coffee:compile','shell:drushall']);
  grunt.registerTask('default', ['dev']);
}

Si necesitáis alguna funcionalidad, o no encontráis lo que estáis buscando, tenéis un gran directorio de plugins aquí.