npm Gruntjs Gulpjs

Si alguna vez hemos realizado un proyecto Node.js, rápidamente nos hace falta algo que nos orqueste cada una de las fases de construcción del proyecto. Necesitamos automatizar ciertas tareas repetitivas como comprobar que nuestro código javascript y css es correcto (lint/csslint), pruebas unitarias, copia de dependencias, minificado, compilado, deploy, etc. Para estas tareas hay herramientas como Gear, Smoosh, Buildr, pero dos destacan sobre las demás, Grunt y Gulp.

En este artículo vamos a comparar ambos frameworks con el que se podría decir más usado de Node.js, su administrador de paquetes npm.

Para ello vamos a crear una nueva aplicación Express y sobre ella vamos a realizar algunas tareas sencillas pero habituales:

  • Limpiar antes de la construcción
  • Compilar CoffeeScript
  • Concatenar
  • Minificar
  • Compilar Less
  • Copiar recursos
  • Lint
  • Test

Breve explicación de los frameworks

Grunt

Es el automatizador de tareas actualmente más utilizado por la comunidad de desarrolladores de Node.js. Tiene más de 4000 plugins disponibles y mucha documentación online. Está basado en la configuración, ¿qué significa esto? que necesitamos un fichero en el que escribir la configuración que luego el código va a utilizar.

Este fichero se llama Gruntfile.js y debe de estar en la raíz de nuestra aplicación. Debe publicar una función que reciba el objeto grunt.

Dentro de esta función deberemos cargar las tareas, registrarlas y configurarlas.

'use strict';

module.exports = function (grunt) {

    // Configuración de cada una de las tareas
    grunt.initConfig({
        ...
    });

    // Carga de las tareas
    grunt.loadNpmTasks('???');
    ...

    // Registro de las tareas
    grunt.registerTask('default', ['???', '???']);

};

Para instalar las dependencias necesarias vamos a utilizar

npm install <name> --save-dev  

Para que nos instale la última version de cada una de las herramientas y ademas nos actualice las dependecias de desarrollo del fichero package.json (devDependencies). Esto será igual para los tres frameworks.

Las dependencias que necesitaremos son:

Gulp

Gulp.js, es algo más joven pero está entrando fuerte. Todavía no llega a las posibilidades de Grunt.js pero cada vez tienen más plugins, ya supera los 1000. Su baza más fuerte con respecto a éste es la velocidad. Es un framework basado en el código en vez de en la configuración, es decir, para ponerlo a funcionar necesitamos programarlo. Esto hace que una gran comunidad lo prefiera ya que es más legible y menos tedioso. Está basado en los Streams de Node.js por lo que las tareas se ejecutan en memoria, esto es mucho más ligero que escribir en disco como lo hace Grunt.js, por lo tanto la ejecución termina siendo mucho más rápida.

Necesitamos también un fichero en la raíz de la aplicación llamado gulpfile.js. En este fichero simplemente importaremos las herramientas que queramos utilizar y luego definiremos las tareas que vayan a utilizarlas.

// Aquí se importarán los plugins necesarios.
// La dependencia imprescindible es gulp.
var gulp = require('gulp');

// Se irán definiendo las tareas una a una
gulp.task('task1', function () {  
    ...
});

// Si una tarea es dependiente de otra, esta última indicaremos la
// como segundo parámetro 
gulp.task('task2', ['task1'], function (.) {  
    ...
});

Al estar basado en los Streams, utilizaremos la función pipe() para enviar la salida de un comando al comando siguiente quedándonos las tareas parecidas a esto:

// Declaramos la tarea, es dependiente de la tarea clean
gulp.task('uglify', ['clean'], function (cb) {

    // Leemos el fichero origen
    gulp.src('myscript.js')

        // Lo minificamos
        .pipe(uglify().on('error', cb))

        // Lo renombramos
        .pipe(rename('myscript.min.js'))

        // Guardamos el resultado en el directorio public
        .pipe(gulp.dest('public'));

    // Pasamos la ejecución a la siguiente tarea
    cb();
});

Una vez creada la tarea ya estaríamos en disposición de llamarla con

gulp <name>  

Para el ejemplo necesitaremos las siguientes dependencias:

npm

Npm es el gestor de paquetes de Node.js. Viene distribuido con él por lo que no tenemos que instalar nada para poder usarlo.

Para poder usar npm como automatizador de tareas debemos saber que son los scripts del npm. Los scripts son una sección dentro del manifest package.json donde podemos definir una serie de items dentro de los cuales podemos meter código de scripting.

{
    "name": "myapp",
    "version": "0.0.1",
    "private": true,

      // Aquí se definen los scripts
    "scripts": {
        "start": "node ./bin/www"    
    },

    "dependencies": {
        "express": "~4.2.0",
        "static-favicon": "~1.0.0",
        ...
      },
      "devDependencies": {
        "less": "^2.2.0",
        "mocha": "^2.1.0",
        ...
      }
}

Al lanzar un script estamos lanzando un Shell y a continuación ejecutando la línea que le hemos indicado al script, por lo tanto en ese script podemos introducir cualquier comando de shell que utilicemos normalmente. Como añadido, npm nos incluye en el PATH del shell el directorio /node_modules/.bin por lo que tendremos accesibles cualquiera de las librerías que hayamos instalado previamente (también las instaladas de forma global -g). También nos proporciona la variable env donde están mapeadas una gran cantidad de variables de entorno que nos podrán servir de mucha utilidad.

Los script se lanzan desde la línea de comandos con

npm run-script [command] [-- <args>]  
npm run [command] [-- <args>]  

El símbolo " - - " nos indica que ahí acaban las opciones y que a partir de ese punto son argumentos que le vamos a pasar a nuestro script, por ejemplo, para este simple script

scripts : {  
    list: "ls"
}
# Ejecutamos el script simple

$ npm run list

> myapp@0.0.1 list d:\repository\nodejs\myapp
> ls

app.js  bin  package.json  public  routes  views

# Ejecutamos con argumentos

$ npm run list -- -la

> myapp@0.0.1 list d:\repository\nodejs\myapp
> ls -la

total 6  
drwxr-xr-x    8 carlos.f Administ     4096 Jan 15 10:46 .  
drwxr-xr-x    1 carlos.f Administ     4096 Jan 15 10:46 ..  
-rw-r--r--    1 carlos.f Administ     1376 Jan 15 10:46 app.js
drwxr-xr-x    3 carlos.f Administ        0 Jan 15 10:46 bin  
-rw-r--r--    1 carlos.f Administ      340 Jan 15 10:46 package.json
drwxr-xr-x    5 carlos.f Administ        0 Jan 15 10:46 public  
drwxr-xr-x    4 carlos.f Administ        0 Jan 15 10:46 routes  
drwxr-xr-x    5 carlos.f Administ        0 Jan 15 10:46 views

Podemos llamar a un script desde otro, podemos pasarle la salida de un script a la entrada de otro con el comando | (pipe), podemos hacer que unos dependan de otros con el comando &&, ejecutarlos asíncronamente con &. Por lo tanto con estas facilidades que nos proporciona gestor de paquetes de node se puede llegar a implementar el proceso de build con el de una manera relativamente sencilla.

Uno de los inconvenientes que tiene el uso del paquete npm es que en Windows no están tan evolucionados como en linux los comandos del shell y pueden causar algún problema y los consiguientes quebraderos de cabeza al usuario.

Implementación del proceso

Vamos a ver como quedaría en cada caso el fichero para cada proceso completo y así comparar.

Gruntfile.js (113 líneas)

'use strict';

module.exports = function (grunt) {

    grunt.initConfig({

        clean: {
            public: ['public/js', 'public/css', 'public/lib'],
            srcs: ['res/js', 'res/css']
        },

        coffee: {
            compile: {
                options: {
                    bare: true
                },
                files: {
                    'res/js/common.js': ['res/coffee/common.coffee'],
                    'res/js/users.js': ['res/coffee/users.coffee']
                }
            }
        },

        concat: {
            public: {
                src: ['res/js/common.js', 'res/js/users.js'],
                dest: 'res/js/script.js'
            }
        },

        uglify: {
            public: {
                src: ['res/js/script.js'],
                dest: 'public/js/script.min.js'
            }
        },

        less: {
            compile: {
                files: {
                    'public/css/style.css': ['res/less/common.less']
                }
            }
        },

        copy: {
            libs: {
                files: [
                    {
                        expand: true,
                        flatten: true,
                        src: ['res/lib/jquery.min.js'],
                        dest: 'public/lib/',
                        filter: 'isFile'
                    }
                ]
            }
        },

        jshint: {
            backend: {
                options: {
                    jshintrc: '.jshintrc'
                },
                files: {
                    src: ['core/**/*.js']
                }
            }
        },

        mochaTest: {
            test: {
                src: ['test/**/*Test.js']
            }
        },

        watch: {
            js: {
                files: 'core/**/*.js',
                tasks: ['jshint']
            },
            css: {
                files: 'res/less/common.less',
                tasks: ['less']
            }
        }

    });

    grunt.loadNpmTasks('grunt-contrib-clean');
    grunt.loadNpmTasks('grunt-contrib-coffee');
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.loadNpmTasks('grunt-contrib-less');
    grunt.loadNpmTasks('grunt-contrib-jshint');
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-mocha-test');

    grunt.registerTask('default',
        [
            'clean',
            'coffee',
            'less',
            'concat',
            'uglify',
            'copy',
            'jshint',
            'mochaTest'
        ]);

};

gulpfile.js (68 líneas)

var gulp = require('gulp'),  
    del = require('del'),
    coffee = require('gulp-coffee'),
    less = require('gulp-less'),
    rename = require("gulp-rename"),
    concat = require("gulp-concat"),
    uglify = require('gulp-uglify'),
    jshint = require('gulp-jshint'),
    mocha = require('gulp-mocha'),
    watch = require('gulp-watch');

gulp.task('clean', function (cb) {  
    del(['public/js', 'public/css', 'public/lib'], cb);
});

gulp.task('coffee', ['clean'], function (cb) {  
    gulp.src(
        [
            'res/coffee/common.coffee',
            'res/coffee/users.coffee'
        ])
        .pipe(coffee({bare: true}).on('error', cb))
        .pipe(concat('scripts.js'))
        .pipe(uglify().on('error', cb))
        .pipe(rename('script.min.js'))
        .pipe(gulp.dest('public/js'));
    cb();
});


gulp.task('less', ['clean'], function (cb) {  
    gulp.src('res/less/common.less')
        .pipe(less().on('error', cb))
        .pipe(rename('style.css'))
        .pipe(gulp.dest('public/css'));
    cb();
});

gulp.task('copy', ['clean'], function () {  
    gulp.src('res/lib/jquery.min.js')
        .pipe(gulp.dest('public/lib/'));
});

gulp.task('jshint', ['clean'], function () {  
    gulp.src('core/**/*.js')
        .pipe(jshint())
        .pipe(jshint.reporter('default'))
        .pipe(jshint.reporter('fail'));
});

gulp.task('mocha', ['clean', 'jshint'], function () {  
    gulp.src('test/**/*Test.js')
        .pipe(mocha())
});

gulp.task('watch', function () {

    watch('core/**/*.js', function () {
        gulp.start('jshint');
    });

    watch('res/less/common.less', function () {
        gulp.start('less');
    });

});

gulp.task('default', ['clean', 'coffee', 'less', 'copy', 'jshint', 'mocha'], function(){});  

scripts en package.json (13 líneas)

{
    "scripts": {
        "start": "node ./bin/www",
        "clean": "rm -rf public/js/* public/css/* public/lib/*",
        "coffee": "coffee -b -c res/coffee/common.coffee res/coffee/users.coffee | " +
                   "cat res/coffee/*.js | uglifyjs -mc > public/js/script.min.js",
        "less": "lessc res/less/common.less > public/css/style.css",
        "copy": "cp res/lib/jquery.min.js public/lib/",
        "jshint": "jshint core/",
        "test": "mocha --recursive test",
        "watch" : "watch \"npm run jshint\" core --wait=1 & ls",
        "default": "npm run clean && npm run coffee && npm run less && " +
                    "npm run copy && npm run jshint && npm run test"
    }
}

Conclusiones

Que decida cada uno que es lo que le puede venir mejor, los tres frameworks son buenos. Si es verdad que el más legible de todos es Gulp, pero al igual que Grunt hay que instalarlo aparte no viene de serie con Node.js. Además ambos paquetes son dependientes completamente de sus plugin, con esto quiero decir que si no metemos plugins ambos frameworks no hacen nada. Parece ser que Grunt empieza a dar problemas con los proyectos grandes o con las versiones en proyectos antiguos.

Por todo esto yo me decanto por npm, si es verdad que es casi el menos legible, pero en cambio no es tan dependiente de los componentes y nos ahorramos un montón de líneas de código.

Espero haberos liado lo suficiente como para que investiguéis un poquito más, saquéis vuestras propias conclusiones y me critiquéis un poco en los comentarios.