domingo, 24 de enero de 2016

Tutorial sobre Gulp.js II: Análisis de Código JavaScript

Introducción

En un post anterior se dio una introducción a Gulp mediante un ejemplo que concatena y minifica de los archivos JavaScript y CSS de un proyecto web.

En esta ocasión se mostrará como Gulp también puede ayudar a automatizar el proceso de análisis estático del código JavaScript mediante JSHint y JSCS.

Debido a que JavaScript es un lenguaje interpretado, dinámico y no tipado tanto la detección de algunos errores que pueden considerarse comunes o el seguimiento de estándares de programación definidos pueden ser procesos complejos, permitiendo que algunas de estas fallas se encuentren cuando el sistema ya está en un ambiente de producción (de hecho también sucede en lenguajes compilados fuertemente tipados).

JSHint es una herramienta que revisa el código JavaScript buscando no sólo errores comunes sino también algunas prácticas configurables, como por ejemplo el uso de comparación estricta (eqeqeq) o limitar el número de bloques anidados (maxdepth), de manera similar a como lo hacen FindBugs o PMD con código Java.

Por otro lado JSCS es una herramienta que revisa el código JavaScript para asegurar que se están siguiendo las reglas de estilos configurados, por ejemplo mantener un número máximo de caracteres en cada línea (maximumLineLength) o el uso de CamelCase en las variables (requireCamelCaseOrUpperCaseIdentifiers). Puede compararse con la herramienta Checkstyle para Java.

Para mantener el ejemplo sencillo se usará como base el proyecto web desarrollado en aquel post anterior sobre Gulp: https://github.com/guillermo-varela/gulp-demo

Nota: en caso de clonar el repositorio desde GitHub, se deben instalar primero las dependencias que ya tiene el proyecto ejecutando:

npm install

JSHint

Para empezar a usarlo se necesita instalar 2 plugins de Gulp:
  • gulp-jshint: Permite usar JSHint desde tareas Gulp.
  • jshint-stylish: Presenta los errores encontrados por JSHint en un reporte detallado.
Ambos se pueden instalar (como dependencias de desarrollo) mediante el siguiente comando:

npm install --save-dev gulp-jshint jshint-stylish

JSHint tiene varias reglas para validar el código fuente, pero también es posible configurarlo bien sea creando un archivo JSON ".jshintrc", creando una propiedad "jshintConfig" en el archivo "package.json" o con comentarios en el propio código JavaScript (directivas) como por ejemplo "/* jshint strict: true */".

En este caso se tendrá un archivo ".jshintrc" en la raíz del proyecto con algunas reglas a manera de ejemplo. El conjunto completo de reglas (opciones) y sus definiciones se puede encontrar en: http://jshint.com/docs/options.

{
  "node": true,
  "browser": true,
  "bitwise": true,
  "curly": true,
  "eqeqeq": true,
  "forin": true,
  "freeze": true,
  "latedef": "nofunc",
  "noarg": true,
  "nonbsp": true,
  "nonew": true,
  "undef": true,
  "unused": true,
  "strict": true,
  "globals": {
    "angular": false,
    "google": false
  }
}

Ya teniendo JSHint instalado y configurado se procede entonces a crear la tarea en Gulp:

gulpfile.js

'use strict';

var gulp    = require('gulp');
var inject  = require('gulp-inject');
var useref  = require('gulp-useref');
var gulpIf  = require('gulp-if');
var uglify  = require('gulp-uglify');
var gutil   = require('gulp-util');
var cssnano = require('gulp-cssnano');
var jshint  = require('gulp-jshint');

// Search for js and css files created for injection in index.html
gulp.task('inject', function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(inject(
      gulp.src('**/*.js', {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(inject(
      gulp.src('**/*.css', {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(gulp.dest('./app'));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['inject'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(useref())
    .pipe(gulpIf('**/*.js', uglify({
      mangle: true
    }).on('error', gutil.log)))
    .pipe(gulpIf('**/*.css', cssnano()))
    .pipe(gulp.dest('./dist'));
});

// Copies the assets into the dist folder
gulp.task('copy:assets', function () {
  return gulp.src('assets*/**', {cwd: './app'})
    .pipe(gulp.dest('./dist'));
});

// Looks for code correctness errors in JS and prints them
gulp.task('jshint', function() {
  return gulp.src('**/*.js', {cwd: './app'})
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'))
    .pipe(jshint.reporter('fail'));
});

Con respecto al archivo "gulpfile.js" original se realizaron los siguientes cambios:
  • Línea 10: Se carga el módulo "gulp-jshint".
  • Líneas 44-48: Se crea una tarea nueva con nombre "jshint" la cual ejecuta JSHint sobre todos los archivos ".js" de la carpeta con el código de la aplicación "app" y lleva los errores encontrados al módulo de reportes "jshint-stylish". Al final se usa ".pipe(jshint.reporter('fail'))" para indicar que en caso de encontrar errores en el código la tarea "jshint" debe terminar en error y no continuar con lo demás, lo cual es muy importante por ejemplo para los ambientes de integración continua (evitar despliegues si hay errores por ejemplo).

Ahora ya es posible ejecutar JSHint con la nueva tarea de Gulp mediante el siguiente comando:

gulp jshint

Using gulpfile /home/user/git/gulp-demo-code-analysis/gulpfile.js
Starting 'jshint'...

app\js\hello.js
  line 2  col 3  Missing "use strict" statement.
  line 4  col 4  Missing semicolon.
  line 1  col 5  'helloWorld' is defined but never used.

  ×  1 error
  ‼  2 warnings


app\js\printer.js
  line 2  col 3  Missing "use strict" statement.
  line 4  col 4  Missing semicolon.
  line 1  col 5  'printer' is defined but never used.

  ×  1 error
  ‼  2 warnings

'jshint' errored after 85 ms
Error in plugin 'gulp-jshint'
Message:
    JSHint failed for: app\js\hello.js, app\js\printer.js

Como puede verse el reporte que presenta "jshint-stylish" es muy detallado al indicar el error, y su ubicación exacta en cada archivo. En este caso advierte sobre la falta de la expresión "use strict", el signo ";" (punto y coma - semicolon) al finalizar y retornar la función creada y su asignación a una variable, que aunque se usa en "index.html" no se usa dentro de dichos archivos, lo cual se resuelve así:

app\js\hello.js
'use strict';

/*jshint unused:false*/
var helloWorld = (function () {
  return function () {
    return 'Hello World...';
  };
})();

app\js\printer.js
'use strict';

/*jshint unused:false*/
var printer = (function () {
  return function (message) {
    return 'Gulp Demo says: ' + message;
  };
})();


Al verificar nuevamente con JSHint se obtiene:
gulp jshint

Using gulpfile /home/user/git/gulp-demo-code-analysis/gulpfile.js
Starting 'jshint'...
Finished 'jshint' after 77 ms

Es de notar que en este caso se usó la directiva "jshint unused:false" para deshabilitar la validación de la regla "unused" desde su ubicación en los respectivos archivos.

JSCS

Esta herramienta también tiene un plugin de npm disponible: gulp-jscs

npm install --save-dev gulp-jscs

Su configuración puede realizarse mediante un archivo JSON llamado ".jscsrc" en la raíz del proyecto. Allí se pueden indicar las reglas que se requieran o inclusive usar una pre-configuración ya definida las cuales están basadas en reglas o recomendaciones que han dado algunas organizaciones como Google, JQuery o Wikimedia, tanto para sus desarrollos internos como para aquellas personas que contribuyen en sus proyectos open-source.

.jscsrc

{
  "preset": "google",
  "maximumLineLength": {
    "value": 160
  }
}

Allí se indica que se usarán las reglas definidas por los estándares de Google, pero se sobrescribe el límite en la longitud de las líneas (maximumLineLength). Inicialmente es 80 y se amplía a 160.

gulpfile.js

'use strict';

var gulp    = require('gulp');
var inject  = require('gulp-inject');
var useref  = require('gulp-useref');
var gulpIf  = require('gulp-if');
var uglify  = require('gulp-uglify');
var gutil   = require('gulp-util');
var cssnano = require('gulp-cssnano');
var jshint  = require('gulp-jshint');
var jscs    = require('gulp-jscs');

// Search for js and css files created for injection in index.html
gulp.task('inject', function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(inject(
      gulp.src('**/*.js', {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(inject(
      gulp.src('**/*.css', {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(gulp.dest('./app'));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['inject'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(useref())
    .pipe(gulpIf('**/*.js', uglify({
      mangle: true
    }).on('error', gutil.log)))
    .pipe(gulpIf('**/*.css', cssnano()))
    .pipe(gulp.dest('./dist'));
});

// Copies the assets into the dist folder
gulp.task('copy:assets', function () {
  return gulp.src('assets*/**', {cwd: './app'})
    .pipe(gulp.dest('./dist'));
});

// Looks for code correctness errors in JS and prints them
gulp.task('jshint', function() {
  return gulp.src('**/*.js', {cwd: './app'})
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'))
    .pipe(jshint.reporter('fail'));
});

// Looks for code style errors in JS and prints them
gulp.task('jscs', function () {
  return gulp.src('**/*.js', {cwd: './app'})
    .pipe(jscs())
    .pipe(jscs.reporter())
    .pipe(jscs.reporter('fail'));
});

Los cambios realizados son:
  • Línea 11: Se carga el módulo "gulp-jscs".
  • Líneas 53-58: Se crea una tarea nueva con nombre "jscs" la cual ejecuta JSCS sobre todos los archivos ".js" de la carpeta con el código de la aplicación "app" y finalmente lleva los errores encontrados al módulo de reportes que tiene por defecto.
Al ejecutar esta nueva tarea se obtiene:

gulp jscs

Using gulpfile /home/user/git/gulp-demo-code-analysis/gulpfile.js
Starting 'jscs'...
Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/hello.js :
     1 |var helloWorld = (function () {
----------------------------------^
     2 |  return function () {
     3 |    return 'Hello World...';

Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/hello.js :
     1 |var helloWorld = (function () {
----------------------------------^
     2 |  return function () {
     3 |    return 'Hello World...';

Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/hello.js :
     1 |var helloWorld = (function () {
     2 |  return function () {
-------------------------^
     3 |    return 'Hello World...';
     4 |  };

Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/hello.js :
     1 |var helloWorld = (function () {
     2 |  return function () {
-------------------------^
     3 |    return 'Hello World...';
     4 |  };


4 code style errors found.
Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/printer.js :
     1 |var printer = (function () {
-------------------------------^
     2 |  return function (message) {
     3 |    return 'Gulp Demo says: ' + message;

Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/printer.js :
     1 |var printer = (function () {
-------------------------------^
     2 |  return function (message) {
     3 |    return 'Gulp Demo says: ' + message;

Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/printer.js :
     1 |var printer = (function () {
     2 |  return function (message) {
-------------------------^
     3 |    return 'Gulp Demo says: ' + message;
     4 |  };

Illegal space before opening round brace at /home/user/git/gulp-demo-code-analysis/app/js/printer.js :
     1 |var printer = (function () {
     2 |  return function (message) {
-------------------------^
     3 |    return 'Gulp Demo says: ' + message;
     4 |  };

4 code style errors found.
'jscs' errored after 288 ms
Error in plugin 'gulp-jscs'
Message:
    JSCS failed for: /home/user/git/gulp-demo-code-analysis/app/js/hello.js, /home/user/git/gulp-demo-code-analysis/app/js/printer.js

Básicamente está reportando que los paréntesis en los que se indican los parámetros de cada función no deben estar precedidos de espacios, lo cual se ajusta de la siguiente manera:

app\js\hello.js
'use strict';

/*jshint unused:false*/
var helloWorld = (function() {
  return function() {
    return 'Hello World...';
  };
})();

app\js\printer.js
'use strict';

/*jshint unused:false*/
var printer = (function() {
  return function(message) {
    return 'Gulp Demo says: ' + message;
  };
})();

Nota: JSCS tiene la opción "fix", la cual permite que automáticamente resuelva este tipo de errores, aunque personalmente prefiero resolver los errores manualmente para revisar caso por caso los reportes y tener mayor control del código desarrollado.


Tarea de Construcción

Ahora que se tienen varias tareas en la configuración de Gulp, se puede crear una sola que agrupe las tareas de análisis de código, concatenación y minificación.

gulp.task('build', ['jshint', 'jscs', 'compress', 'copy:assets']);

Al ejecutar esta tarea se tiene este resultado:
gulp build

Using gulpfile /home/user/git/gulp-demo-code-analysis/gulpfile.js
Starting 'jshint'...
Starting 'jscs'...
Starting 'inject'...
Starting 'copy:assets'...
Finished 'copy:assets' after 8.72 ms
Finished 'jscs' after 838 ms
gulp-inject 2 files into index.html.
gulp-inject 2 files into index.html.
Finished 'jshint' after 1.18 s
Finished 'inject' after 288 ms
Starting 'compress'...
Finished 'compress' after 2.9 s
Starting 'build'...
Finished 'build' after 19 µs

La tarea "inject" fue ejecutada a pesar de no estar incluida explicitamente en "build" debido a que es una dependencia de "compress". También puede apreciarse que las tareas "copy:assets", "jshint" y "jscs" fueron ejecutadas simultáneamente ya que no hay dependencias entre estas.

Cuando se empieza a tener muchas tareas de Gulp se vuelve dificil hacer seguimiento a las dependencias de cada tarea con la sintaxis actual:

gulp.task('taskName', ['dependency1', 'dependency2', ...], function () {
  return ...
});

Para Gulp 4.0 se planea tener una manera alternativa de declarar una lista de tareas que se deben ejecutar secuencialmente, en un orden determinado:

gulp.task('taskName', gulp.series('task1', 'task2', function(done) {
  done();
}));

Al momento de escribir este post la versión estable de Gulp es 3.9.0, así que mientras tanto se usará como alternativa "run-sequence".

npm install --save-dev run-sequence

gulpfile.js

'use strict';

var gulp        = require('gulp');
var inject      = require('gulp-inject');
var useref      = require('gulp-useref');
var gulpIf      = require('gulp-if');
var uglify      = require('gulp-uglify');
var gutil       = require('gulp-util');
var cssnano     = require('gulp-cssnano');
var jshint      = require('gulp-jshint');
var jscs        = require('gulp-jscs');
var runSequence = require('run-sequence');

// Search for js and css files created for injection in index.html
gulp.task('inject', function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(inject(
      gulp.src('**/*.js', {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(inject(
      gulp.src('**/*.css', {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(gulp.dest('./app'));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['inject'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(useref())
    .pipe(gulpIf('**/*.js', uglify({
      mangle: true
    }).on('error', gutil.log)))
    .pipe(gulpIf('**/*.css', cssnano()))
    .pipe(gulp.dest('./dist'));
});

// Copies the assets into the dist folder
gulp.task('copy:assets', function () {
  return gulp.src('assets*/**', {cwd: './app'})
    .pipe(gulp.dest('./dist'));
});

// Looks for code correctness errors in JS and prints them
gulp.task('jshint', function() {
  return gulp.src('**/*.js', {cwd: './app'})
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'))
    .pipe(jshint.reporter('fail'));
});

// Looks for code style errors in JS and prints them
gulp.task('jscs', function () {
  return gulp.src('**/*.js', {cwd: './app'})
    .pipe(jscs())
    .pipe(jscs.reporter())
    .pipe(jscs.reporter('fail'));
});

gulp.task('build', function (done) {
  runSequence('jshint', 'jscs', 'compress', 'copy:assets', done);
});

Los cambios realizados son:
  • Línea 12: Se carga el módulo "run-sequence".
  • Líneas 61-63: Se crea una tarea nueva con nombre "build" la cual ejecuta el análisis del código JavaScript ("jshint", "jscs") y posteriormente la concatenación y minificación ("compress") de manera secuencial.

Al usar esta nueva tarea se tiene el siguiente resultado:
gulp build

Using gulpfile /home/user/git/gulp-demo-code-analysis/gulpfile.js
Starting 'build'...
Starting 'jshint'...
Finished 'jshint' after 89 ms
Starting 'jscs'...
Finished 'jscs' after 254 ms
Starting 'inject'...
gulp-inject 2 files into index.html.
gulp-inject 2 files into index.html.
Finished 'inject' after 22 ms
Starting 'compress'...
Finished 'compress' after 397 ms
Starting 'copy:assets'...
Finished 'copy:assets' after 1.16 ms
Finished 'build' after 773 ms

Como puede verse todas las tareas se ejecutaron dentro de "build" de manera secuencial y ordenada (una tarea sólo comenzó cuando la anterior terminó).


Limpieza de la Carpeta de Construcción

Aprovechando que ahora la construcción (build) se ejecuta siguiente un orden definido, puede incluirse una tarea adicional que borre el contenido de la carpeta "dist" antes de que se empiecen a copiar los archivos, con el fin de evitar que queden archivos que fueron borrados o re-nombrados en "app", sin correr el riesgo de borrar los archivos que se están copiando.

Para esto se usará el módulo "del". Aunque no es un plugin realmente desarrollado para Gulp, al ser un módulo/paquete disponible desde npm se puede usar también en tareas de Gulp.

npm install --save-dev del

gulpfile.js
'use strict';

var gulp        = require('gulp');
var inject      = require('gulp-inject');
var useref      = require('gulp-useref');
var gulpIf      = require('gulp-if');
var uglify      = require('gulp-uglify');
var gutil       = require('gulp-util');
var cssnano     = require('gulp-cssnano');
var jshint      = require('gulp-jshint');
var jscs        = require('gulp-jscs');
var del         = require('del');
var runSequence = require('run-sequence');

// Search for js and css files created for injection in index.html
gulp.task('inject', function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(inject(
      gulp.src('**/*.js', {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(inject(
      gulp.src('**/*.css', {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(gulp.dest('./app'));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['inject'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(useref())
    .pipe(gulpIf('**/*.js', uglify({
      mangle: true
    }).on('error', gutil.log)))
    .pipe(gulpIf('**/*.css', cssnano()))
    .pipe(gulp.dest('./dist'));
});

// Copies the assets into the dist folder
gulp.task('copy:assets', function () {
  return gulp.src('assets*/**', {cwd: './app'})
    .pipe(gulp.dest('./dist'));
});

// Looks for code correctness errors in JS and prints them
gulp.task('jshint', function() {
  return gulp.src('**/*.js', {cwd: './app'})
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'))
    .pipe(jshint.reporter('fail'));
});

// Looks for code style errors in JS and prints them
gulp.task('jscs', function () {
  return gulp.src('**/*.js', {cwd: './app'})
    .pipe(jscs())
    .pipe(jscs.reporter())
    .pipe(jscs.reporter('fail'));
});

// Cleans the dist folder
gulp.task('clean:dist', function () {
  return del('dist/**/*');
});

gulp.task('build', function (done) {
  runSequence('jshint', 'jscs', 'clean:dist', 'compress', 'copy:assets', done);
});

Los cambios realizados son:
  • Línea 12: Se carga el módulo "del".
  • Líneas 63-65: Se crea una tarea nueva con nombre "clean:dist" la cual borra el contenido dentro de "dist".
  • Línea 68: Adición de la nueva tarea en "build".

Ejecutando nuevamente la construcción se tiene:
gulp build

Using gulpfile /home/user/git/gulp-demo-code-analysis/gulpfile.js
Starting 'build'...
Starting 'jshint'...
Finished 'jshint' after 79 ms
Starting 'jscs'...
Finished 'jscs' after 268 ms
Starting 'clean:dist'...
Finished 'clean:dist' after 19 ms
Starting 'inject'...
gulp-inject 2 files into index.html.
gulp-inject 2 files into index.html.
Finished 'inject' after 31 ms
Starting 'compress'...
Finished 'compress' after 382 ms
Starting 'copy:assets'...
Finished 'copy:assets' after 4.91 ms
Finished 'build' after 795 ms

El proyecto completo puede descargarse desde: https://github.com/guillermo-varela/gulp-demo-code-analysis

Conclusiones

Con algunas cuantas tareas de Gulp adicionales se ha logrado tener un flujo de construcción para un proyecto web que, aunque en el presente ejemplo es bastante pequeño, puede ser aplicado en proyectos de mayor tamaño sin necesidad de cambio alguno.

En el próximo post se mostrará cómo se puede también automatizar la actualización de los cambios que se hacen durante el desarrollo.

Referencias

http://jshint.com/docs
http://jshint.com/docs/options
https://github.com/jshint/jshint
https://www.npmjs.com/package/gulp-jshint
http://jscs.info
https://google.github.io/styleguide/javascriptguide.xml
https://www.npmjs.com/package/gulp-jscs
https://scotch.io/tutorials/automate-your-tasks-easily-with-gulp-js
https://github.com/gulpjs/gulp/blob/master/docs/recipes/delete-files-folder.md

No hay comentarios.:

Publicar un comentario