Mostrando las entradas con la etiqueta Gulp. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Gulp. Mostrar todas las entradas

lunes, 22 de febrero de 2016

Tutorial sobre Gulp.js IV: Algunas Prácticas Recomendadas

Introducción

En posts anteriores se ha venido trabajando con Gulp.js, mostrando cómo esta herramienta puede ayudar en la automatización de tareas en los proyectos web, análisis de código JavaScript, actualización automática de cambios en el navegador web e inclusive su posibilidad de integrarse con otras herramientas como Bower.

En esta ocasión se mostrarán un par de prácticas recomendadas al usar Gulp.js que pueden ser de utilidad tanto en proyectos grandes como pequeños.

Para ahorrar algo de tiempo se tomará como base el proyecto desarrollado en el último post introductorio sobre Bower: https://github.com/guillermo-varela/gulp-bower-demo

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

npm install

Separar las Librerías Externas del Código del Proyecto

Actualmente el equipo de desarrollo de AngularJS está recomendando seguir la guía acerca de estilos y estándares propuestos por John Papahttps://github.com/johnpapa/angular-styleguide

Una de las recomendaciones allí aparecen es separar el código de las librerías externas del código desarrollado para el proyecto.

En el proyecto desarrollado previamente se tiene un archivo ".bowerrc" en el cual se indica que las librerías mediante Bower se descargarán en "app/lib" y es en "app/" donde se encuentra el código del proyecto.

Para seguir esta recomendación se borrará el archivo ".bowerrc", con lo cual al ejecutar "bower install" las librerías se descargarán en la raíz del proyecto en "bower_components/".

Figura 1 - Cambio en la Ubicación de las Librerías de Bower

En ".gitignore" se debe cambiar la entrada "app/lib/" por "bower_components/" para que las dependencias descargadas mediante Bower no se incluyan en el repositorio de código.

También deben hacerse algunos cambios en "gulpfile.js":
'use strict';

var gulp        = require('gulp');
var inject      = require('gulp-inject');
var wiredep     = require('wiredep').stream;
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 connect     = require('gulp-connect');
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'));
});

// Inject libraries via Bower in between of blocks "bower:xx" in index.html
gulp.task('wiredep', function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(wiredep({
      'ignorePath': '..'
    }))
    .pipe(gulp.dest('./app'));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['inject', 'wiredep'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(useref({ searchPath: ['./', './app'] }))
    .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/**/*');
});

// Watch changes on application files
gulp.task('watch', function() {
  gulp.watch(['**/*.css'], {cwd: './app'}, ['inject']);
  gulp.watch(['**/*.js'], {cwd: './app'}, ['jshint', 'jscs', 'inject']);
  gulp.watch(['./bower.json'], ['wiredep']);
  gulp.watch('**/*.html', {cwd: './app'}, function(event) {
    gulp.src(event.path)
      .pipe(connect.reload());
  });
});

// Starts a development web server
gulp.task('server', function () {
  connect.server({
    root: './app',
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true
  });
});

// Starts a server using the production build
gulp.task('server-dist', ['build'], function () {
  connect.server({
    root: './dist',
    hostname: '0.0.0.0',
    port: 8080
  });
});

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

gulp.task('default', ['inject', 'wiredep', 'server', 'watch']);

Los cambios son:

  • Líneas 21, 25, 59, 67, 80, 81: Dado que ahora las librerías descargadas mediante Bower están en una carpeta por fuera de "app/", ya no es necesario excluir el código de estas librerías de las tareas de Gulp que sólo deben usar el código de la aplicación, como por ejemplo "inject", "jshint", "jscs" y "watch". Esta es una gran ventaja que tiene el seguir esta recomendación ya que la configuración de las tareas de Gulp se hace más sencilla.
  • Línea 35: En la tarea "wiredep" se quita la propiedad "directory: './app/lib/'" ya que la ubicación de las librerías de Bower es la que se usa por defecto. En su lugar se incluye "'ignorePath': '..'" ya que de no hacerlo las referencias en "index.html", al ser URLs relativas tendrían la indicación de carpeta anterior "../", pero debido a que en las tareas "server" y "server-dist" exponen sólo el contenido de la carpeta "app/" (usando gulp-connect) ello generaría errores de recursos no encontrados (404). El resultado sería:
    • ../bower_components/jquery/dist/jquery.js => /bower_components/jquery/dist/jquery.js
    • ../bower_components/bootstrap/dist/js/bootstrap.js  => /bower_components/bootstrap/dist/js/bootstrap.js
  • Línea 43: En la tarea "compress", al usar el plugin "gulp-useref" para concatenar los recursos (JavaScript y CSS) referenciados en "index.html", se incluye la propiedad "searchPath" para indicar que los archivos se deben buscar tanto en la raíz "./" (para encontrar "bower_components") como en "./app".

Al iniciar el servidor de desarrollo, usando el comando "gulp", se podrá ver lo siguiente:
Figura 2 - Aplicación Web sin Dependencias Bower

Al revisar el archivo "index.html" se podrá ver que las dependencias Bower se están indicando con la raíz "/bower_components/" lo cual no existe desde la carpeta "./app/" que se está exponiendo en el servidor web. Para solucionar esto se requiere dejar disponible el contenido de "/bower_components/" como parte de la raíz del servidor web, lo cual se puede hacer usando el plugin "st", el cual expone contenido estático en un servidor web.

Para instalarlo se ejecuta el siguiente comando:

npm install --save-dev st

Para usarlo se debe incluir en "gulpfile.js":

...
var st          = require('st');
...

// Starts a development web server
gulp.task('server', function () {
  connect.server({
    root: './app',
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true,
    middleware: function (connect, opt) {
      return [
        st({
          path: 'bower_components',
          url: '/bower_components'
        })
      ];
    }
  });
});

En la tarea de Gulp "server" se le agrega la propiedad "middleware" a "gulp-connect" para que usando "st" se exponga el contenido de "bower_components" como "/bower_components", justo como se tiene en "index.html". Al ejecutar de nuevo "gulp" se puede ver que la página ya carga con los estilos de Bootstrap.
Figura 3 - Aplicación Web con las Dependencias de Bower

Carga Automática de Plugins de Gulp

Hasta este momento al inicio del archivo "gulpfile.js" se tiene la declaración en variables de los plugins de npm que se usan actualmente para las tareas de Gulp del proyecto.

Para hacer que esta sección sea un poco más corta y que por ende el archivo "gulpfile.js" sea más fácil de entender se puede usar el plugin "gulp-load-plugins", el cual carga automáticamente todos los plugins de Gulp declarados en "package.json" y los deja disponible dentro de una variable que se defina. Por ejemplo si se usa la variable "plugins" entonces cada plugin se accede como un método de dicha variable quitando el prefijo "gulp-" y en Camel Case (para los plugins que lleven guión en su nombre), así para usar "gulp-inject" se tendría "plugins.inject".

De esta manera en el archivo "gulpfile.js" se tendría:
'use strict';

var gulp        = require('gulp');
var plugins     = require('gulp-load-plugins')();
var wiredep     = require('wiredep').stream;
var del         = require('del');
var st          = require('st');
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: paths.app})
    .pipe(plugins.inject(
      gulp.src(paths.js, {cwd: paths.app, read: false}), {
        relative: true
      }))
    .pipe(plugins.inject(
      gulp.src(paths.css, {cwd: paths.app, read: false}), {
        relative: true
      }))
    .pipe(gulp.dest(paths.app));
});

// Inject libraries via Bower in between of blocks "bower:xx" in index.html
gulp.task('wiredep', ['inject'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(wiredep({
      'ignorePath': '..'
    }))
    .pipe(gulp.dest('./app'));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['wiredep'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(plugins.useref({ searchPath: ['./', './app'] }))
    .pipe(plugins.if('**/*.js', plugins.uglify({
      mangle: true
    }).on('error', plugins.util.log)))
    .pipe(plugins.if('**/*.css', plugins.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(plugins.jshint())
    .pipe(plugins.jshint.reporter('jshint-stylish'))
    .pipe(plugins.jshint.reporter('fail'));
});

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

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

// Watch changes on application files
gulp.task('watch', function() {
  gulp.watch(['**/*.css'], {cwd: './app'}, ['inject']);
  gulp.watch(['**/*.js'], {cwd: './app'}, ['jshint', 'jscs', 'inject']);
  gulp.watch(['./bower.json'], ['wiredep']);
  pgulp.watch('**/*.html', {cwd: './app'}, function(event) {
    gulp.src(event.path)
      .pipe(plugins.connect.reload());
  });
});

// Starts a development web server
gulp.task('server', function () {
  plugins.connect.server({
    root: './app',
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true,
    middleware: function (connect, opt) {
      return [
        st({
          path: 'bower_components',
          url: '/bower_components'
        })
      ];
    }
  });
});

// Starts a server using the production build
gulp.task('server-dist', ['build'], function () {
  plugins.connect.server({
    root: './dist',
    hostname: '0.0.0.0',
    port: 8080
  });
});

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

gulp.task('default', ['inject', 'wiredep', 'server', 'watch']);

Nota: Los plugins que no son de Gulp (sin prefijo "gulp-") aún deben cargarse explícitamente.

Externalización de Rutas

Para evitar repetir constantemente el mismo conjunto de rutas (globs) y/o archivos, es posible tenerlos en variables aparte o inclusive en un archivo adicional, para que "gulpfile.js" no quede más largo y más fácil de leer y mantener, especialmente si el proyecto tiene una estructura de archivos más compleja (como por ejemplo https://github.com/johnpapa/ng-demos/blob/master/modular/gulp.config.json y https://github.com/johnpapa/ng-demos/blob/master/modular/gulpfile.js).

"gulp-paths.json"
{
  "app": "./app/",
  "js": ["**/*.js"],
  "css": ["**/*.css"],
  "dist": "./dist/"
}

Para los valores que correspondan a carpetas, se recomienda terminar en "/" para mantener consistencia en su uso.

"gulpfile.js"

'use strict';

var gulp        = require('gulp');
var plugins     = require('gulp-load-plugins')();
var paths       = require('./gulp-paths.json');
var wiredep     = require('wiredep').stream;
var del         = require('del');
var st          = require('st');
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: paths.app})
    .pipe(plugins.inject(
      gulp.src(paths.js, {cwd: paths.app, read: false}), {
        relative: true
      }))
    .pipe(plugins.inject(
      gulp.src(paths.css, {cwd: paths.app, read: false}), {
        relative: true
      }))
    .pipe(gulp.dest(paths.app));
});

// Inject libraries via Bower in between of blocks "bower:xx" in index.html
gulp.task('wiredep', ['inject'], function () {
  return gulp.src('index.html', {cwd: paths.app})
    .pipe(wiredep({
      'ignorePath': '..'
    }))
    .pipe(gulp.dest(paths.app));
});

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

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

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

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

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

// Watch changes on application files
gulp.task('watch', function() {
  gulp.watch(paths.css, {cwd: paths.app}, ['inject']);
  gulp.watch(paths.js, {cwd: paths.app}, ['jshint', 'jscs', 'inject']);
  gulp.watch(['./bower.json'], ['wiredep']);
  gulp.watch('**/*.html', {cwd: paths.app}, function(event) {
    gulp.src(event.path)
      .pipe(plugins.connect.reload());
  });
});

// Starts a development web server
gulp.task('server', function () {
  plugins.connect.server({
    root: paths.app,
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true,
    middleware: function (connect, opt) {
      return [
        st({
          path: 'bower_components',
          url: '/bower_components'
        })
      ];
    }
  });
});

// Starts a server using the production build
gulp.task('server-dist', ['build'], function () {
  plugins.connect.server({
    root: paths.dist,
    hostname: '0.0.0.0',
    port: 8080
  });
});

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

gulp.task('default', ['inject', 'wiredep', 'server', 'watch']);

Los cambios fueron:

  • Línea 5: Se carga el archivo "gulp-paths.json" como un objeto JSON en la variable "paths".
  • Líneas 13, 15, 19, 22, 27, 31, 36, 37, 47, 53, 61, 74, 75, 77 y 86: Se reemplazó la cadena "./app" por "paths.app", para usar el valor indicado en "gulp-paths.json".
  • Líneas 15, 53, 61 y 75: Se reemplazó el arreglo "['**/*.js']" por "paths.js", para usar el valor indicado en "gulp-paths.json".
  • Líneas 19 y 74: Se reemplazó el arreglo "['**/*.css']" por "paths.css", para usar el valor indicado en "gulp-paths.json".
  • Líneas 42, 48, 69 y 104: Se reemplazó la cadena "./dist" por "paths.dist", para usar el valor indicado en "gulp-paths.json". Nótese que en la línea 69 se está concatenando "paths.dist" con "'**/*'" para indicar que se debe borrar el contenido y no la carpeta "dist" como tal y en este caso al tener como estándar el finalizar los valores de las carpetas con "/" se sabe que no es necesario incluir este caracter adicional.

Nota: En la tarea "compress" no se reemplazaron las cadenas "**/*.js" ni "**/*.css" ya que en estos casos no se están indicando rutas sino tipos de archivos.

El proyecto completo se puede descargar desde: https://github.com/guillermo-varela/gulp-bower-demo-ii

Referencias

https://github.com/johnpapa/angular-styleguide
https://www.npmjs.com/package/st
https://www.npmjs.com/package/gulp-load-plugins

lunes, 8 de febrero de 2016

Tutorial sobre Bower: Administración de Dependencias Web

https://github.com/bower/bower.github.io/blob/master/img/bower-logo.png

Introducción

Actualmente existen muchos frameworks y librerías disponibles para proyectos web, como por ejemplo AngularJSEmber.jsBackbone.js, Bootstrap, JQuery, entre muchos otros; inclusive se tienen dependencias entre librerías, por ejemplo Bootstrap depende de JQuery.

Cada librería puede incluirse en un proyecto bien sea descargando directamente los archivos necesarios desde su página oficial e incluyéndolos en una carpeta del proyecto o usando referencias hacia un CDN (Content Delivery Network), sin embargo esto hace que se deban incluir y mantener manualmente las dependencias de cada librería.

Bower es una herramienta que permite no solamente descargar librerías para proyectos web sino también tener en cuenta las dependencias que tienen para descargarlas también o advertir que el proyecto tiene una versión de dicha dependencia incompatible. En cierta medida es similar a npm o RubyGems.

Para demostrar su funcionamiento se tomará como base el proyecto desarrollado en el último post introductorio sobre Gulp y se adicionará Bootstrap: https://github.com/guillermo-varela/gulp-demo-watch-live

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

npm install

Instalación

Bower es un módulo de Node.js, por lo cual se requiere como pre-requisito tener instalado Node.js bien sea con el instalador oficial o mediante Node Version Manager (nvm), como se indica en el post sobre Gulp.

Una vez instalado Node.js se puede instalar Bower globalmente y comprobar la instalación mediante:

npm install -g bower

bower -v
1.7.7

Nota: Para el caso de Bower, no se requiere instalarlo a nivel del proyecto, ya que no se usará su API JavaScript en la construcción del proyecto.

Configuración de Bower

Inicialización

El comando "bower init" permite crear el archivo "bower.json" en el cual se tendrá la configuración de Bower, así como las dependencias usadas. Para ello realizará una serie de preguntas, algunas de las cuales tienen valores por defecto tomadas del archivo "package.json" (si existe) o de la configuración del repositorio Git (si se tiene).

bower init

? name gulp-bower-demo
? description Just a demo project using Bower and Gulp.
? main file
? what types of modules does this package expose?
? keywords bower, bootstrap, gulp
? authors
? license MIT
? homepage https://github.com/guillermo-varela/gulp-bower-demo
? set currently installed components as dependencies? No
? add commonly ignored files to ignore list? No
? would you like to mark this package as private which prevents it from being accidentally published to the registry? Yes

{
  name: 'gulp-bower-demo',
  description: 'Just a demo project using Bower and Gulp.',
  main: '',
  license: 'MIT',
  keywords: [
    'bower',
    'bootstrap',
    'gulp'
  ],
  homepage: 'https://github.com/guillermo-varela/gulp-bower-demo',
  moduleType: [],
  private: true
}

? Looks good? Yes

Al finalizar se debe tener un archivo "bower.json" con el contenido indicado en la confirmación del comando "init".

Carpeta de Instalación de Dependencias

Por defecto las librerías que se instalen como dependencias del proyecto se almacenarán en una carpeta en la raíz del proyecto llamada "bower_components".

Debido a que en este caso el código de la aplicación se encuentra dentro de la carpeta "app" se cambiará la ubicación del directorio de instalación de Bower creando un archivo JSON llamado ".bowerrc" indicando la ruta que se quiere:

{
  "directory": "app/lib"
}

Nota: Para reducir el tamaño ocupado en los repositorios de código, esta carpeta debe ser excluida del sistema de versionamiento.

Administración de Dependencias

Instalando Paquetes

Para instalar una librería como dependencia del proyecto se usa el comando "bower install <paquete>". El paquete a instalar puede ser una URL, un repositorio o un paquete registrado en el sitio oficial de Bower:

# registered package
bower install --save jquery

# GitHub shorthand
bower install --save user/repository

# Git endpoint
bower install --save git://github.com/user/package.git

# URL
bower install --save http://example.com/script.js

El flag "--save" sirve para indicar que adicionalmente a descargar el paquete se quiere también registrar la dependencia en el archivo "bower.json". De esta manera al descargar el proyecto sin las dependencias estas podrán ser descargadas mediante el comando "bower install".

Así al instalar Bootstrap por ejemplo (bower install --save bootstrap) se agrega una nueva entrada en "bower.json" con el nombre "dependencies":

{
  "name": "gulp-bower-demo",
  "description": "Just a demo project using Bower and Gulp.",
  "license": "MIT",
  "keywords": [
    "bower",
    "bootstrap",
    "gulp"
  ],
  "homepage": "https://github.com/guillermo-varela/gulp-bower-demo",
  "moduleType": [],
  "private": true,
  "dependencies": {
    "bootstrap": "^3.3.6"
  }
}

De manera opcional puede indicarse la versión del paquete que se quiere instalar de la siguiente manera: "bower install <paquete>#version". En caso de no indicar la versión Bower instalará la versión más reciente.

Información de los Paquetes

Antes de instalar una dependencia/paquete/librería puede obtenerse información acerca de esta ejecutando "bower info <paquete>" (también se tiene la opción de indicar la versión), por ejemplo para el caso de Bootstrap:

bower info bootstrap

{
  name: 'bootstrap',
  description: 'The most popular front-end framework for developing responsive, mobile first projects on the web.',
  keywords: [
    'css',
    'js',
    'less',
    'mobile-first',
    'responsive',
    'front-end',
    'framework',
    'web'
  ],
  homepage: 'http://getbootstrap.com',
  license: 'MIT',
  moduleType: 'globals',
  main: [
    'less/bootstrap.less',
    'dist/js/bootstrap.js'
  ],
  ignore: [
    '/.*',
    '_config.yml',
    'CNAME',
    'composer.json',
    'CONTRIBUTING.md',
    'docs',
    'js/tests',
    'test-infra'
  ],
  dependencies: {
    jquery: '1.9.1 - 2'
  },
  version: '3.3.6'
}

Available versions:
  - 3.3.6
  - 3.3.5
  - 3.3.4
  - 3.3.2
  - 3.3.1
  - 3.3.0
  - 3.2.0
  - 3.1.1
  - 3.1.0
  - 3.0.3
  - 3.0.2
  - 3.0.1
  - 3.0.0
  - 2.3.2
  - 2.3.1
  - 2.3.0
  - 2.2.2
  - 2.2.1
  - 2.2.0
  - 2.1.1
  - 2.1.0
  - 2.0.4
  - 2.0.3
  - 2.0.2
  - 2.0.1
  - 2.0.0
  - 1.4.0
  - 1.3.0
  - 1.2.0
  - 1.1.1
  - 1.1.0
  - 1.0.0

Show 4 additional prereleases with 'bower info bootstrap --verbose'
You can request info for a specific version with 'bower info bootstrap#'

En la parte resaltada puede verse que para la versión más reciente (3.3.6) se tiene una dependencia con JQuery desde la versión 1.9.1 hasta 2.

Conflicto de Dependencias

Si se intenta instalar Bootstrap pero el proyecto ya está usando una versión distinta de JQuery, Bower preguntará cuál se debe usar:


bower install --save jquery#1.8.9
bower install --save bootstrap

bower bootstrap#*               cached git://github.com/twbs/bootstrap.git#3.3.6
bower bootstrap#*             validate 3.3.6 against git://github.com/twbs/bootstrap.git#*
bower jquery#1.9.1 - 2          cached git://github.com/jquery/jquery-dist.git#2.2.0
bower jquery#1.9.1 - 2        validate 2.2.0 against git://github.com/jquery/jquery-dist.git#1.9.1 - 2

Unable to find a suitable version for jquery, please choose one:
    1) jquery#1.8.0 which resolved to 1.8.0 and is required by gulp-bower-demo
    2) jquery#1.9.1 - 2 which resolved to 2.2.0 and is required by bootstrap#3.3.6

Prefix the choice with ! to persist it to bower.json

? Answer !2
bower jquery                resolution Saved jquery#1.9.1 - 2 as resolution
bower jquery#1.9.1 - 2         install jquery#2.2.0
bower bootstrap#^3.3.6         install bootstrap#3.3.6

jquery#2.2.0 bower_components\jquery

bootstrap#3.3.6 bower_components\bootstrap
└── jquery#2.2.0

En caso de escoger usar la versión compatible con la librería que se quiere instalar, como se hizo en este ejemplo, Bower cambiará la versión que se está usando de la librería anterior por la que necesita para la nueva dependencia en la carpeta de instalación de paquetes, pero en "bower.json" se seguirá indicando que el proyecto necesita la otra versión. Es por esto que lo más recomendable en estos casos es instalar primero la versión de la primera librería que sea compatible con la nueva.

Lista de Paquetes Instalados

El comando "bower list" permite ver los paquetes instalados en el proyecto:

bower list

bower check-new     Checking for new versions of the project dependencies...
gulp-bower-demo /home/user/git/gulp-bower-demo
├─┬ bootstrap#3.3.6 (latest is 4.0.0-alpha.2)
│ └── jquery#2.2.0 (latest is 3.0.0-beta1)
└── jquery#2.2.0 incompatible with 1.8.0 (1.8.0 available, latest is 3.0.0-beta1)

Desinstalar Paquetes

Así como se pueden instalar paquetes, también se pueden desinstalar del proyecto ejecutando "bower uninstall --save <paquete>". Así, para desinstalar JQuery y Bootstrap se debe ejecutar:

bower uninstall --save bootstrap jquery

bower uninstall     bootstrap
bower uninstall     jquery

Otras funcionalidades se pueden encontrar en el API oficial de Bower.

Desarrollo del Proyecto

Como primer paso se debe instalar Bootstrap mediante Bower:

bower install --save bootstrap

Luego se procede a incluir Bootstrap en la página "index.html". Para ese ejemplo se usará como base el ejemplo más sencillo que provee Bootstrap: http://getbootstrap.com/examples/starter-template

app/index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Gulp-Bower Demo">
  <link rel="icon" href="assets/img/favicon.ico">

  <title>Gulp-Bower Demo</title>

  <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css">

  <!-- build:css css/styles.min.css -->
  <!-- inject:css -->
  <link rel="stylesheet" href="css/style1.css">
  <link rel="stylesheet" href="css/style2.css">
  <!-- endinject -->
  <!-- endbuild -->

  <script src="lib/jquery/dist/jquery.min.js"></script>
  <script src="lib/bootstrap/dist/js/bootstrap.min.js"></script>

  <!-- build:js js/scripts.min.js -->
  <!-- inject:js -->
  <script src="js/hello.js"></script>
  <script src="js/printer.js"></script>
  <!-- endinject -->
  <!-- endbuild -->
</head>

<body>
  <nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">

      <div class="navbar-header">
        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">Gulp-Bower Demo</a>
      </div>

      <div id="navbar" class="collapse navbar-collapse">
        <ul class="nav navbar-nav">
          <li class="active"><a href="#">Home</a></li>
          <li><a href="#about">About</a></li>
          <li><a href="#contact">Contact</a></li>
        </ul>
      </div>

    </div>
  </nav>

  <div class="container">
    <div class="title">
      <h1>Bootstrap-Gulp-Bower starter template</h1>
      <p class="lead">
        Use this document as a way to quickly start any new project.
      </p>
    </div>

    <span id="first" class="text1"></span>
    <br/>
    <span id="second" class="text2"></span>
  </div>

  <script>
    document.getElementById('first').innerHTML = helloWorld();
    document.getElementById('second').innerHTML = printer('Hello World');
  </script>
</body>

</html>

Los cambios fueron:
  • Línea 13: Se incluye la referencia a los estilos de Bootstrap.
  • Líneas 22-23: Se incluyen las referencias a los archivos JavaScript de JQuery y Bootstrap.
  • Líneas 34-56: Para mostrar el funcionamiento de Bootstrap, se copió el menú superior que se tiene en la página oficial de ejemplo.

En este ejemplo las referencias a los archivos JavaScript de JQuery y Bootstrap se están indicando dentro de la etiqueta "head".

En la documentación de Bootstrap y algunos otros sitios puede encontrarse que los archivos JavaScript se referencian justo antes de cerrar la etiqueta "body", lo cual tiene como origen una recomendación de Yahoo para dar la impresión a los usuarios de una carga más rápida de las páginas web, lo cual se ha transformado en una cuestión de gustos personales, ya que se encuentran opiniones a favor y en contra.

Personalmente, para páginas web que no tengan funcionalidades indispensables en JavaScript estas referencias pueden estar al final, ya que la página se puede mostrar al usuario mientras dichos archivos cargan sin perder mayor funcionalidad. Sin embargo para aplicaciones web que sí dependan de JavaScript para su funcionamiento encuentro que es mejor referenciar los archivos JavaScript al principio, ya que cuando la página se le muestre al usuario se tendrá un sitio funcional, en lugar de botones u opciones que no hacen nada. Este razonamiento se ilustra un poco más en el siguiente artículo: http://demianlabs.com/lab/post/top-or-bottom-of-the-page-where-should-you-load-your-javascript


app/css/style1.css
body {
  padding-top: 50px;
}

.title {
  text-align: center;
}

.text1 {
    color: red;
}

Los cambios fueron:
  • Líneas 1-3: Para que el contenido aparezca bajo el nuevo menú superior, se agrega un espacio de 50 píxeles.
  • Líneas 5-7: Se crea una clase para el título para que el texto esté centrado.

Al abrir el archivo "index.html" en un navegador web puede verse que ya se aplican los estilos de Bootstrap y la funcionalidad del menú tanto en navegadores web de PC como en dispositivos móviles.
Figura 1 - Página en navegador web

Figura 2 - Página en navegador web emulando un iPhone

Integración de Bower con Gulp

Inyección de Archivos

Aprovechando que el proyecto ya está usando Gulp, se mostrará cómo se pueden incluir las referencias a los archivos CSS y JavaScript automáticamente usando el plugin "wiredep", el cual inyecta los archivos declarados en la propiedad "main" de cada paquete y se puede instalar con el siguiente comando:

npm install --save-dev wiredep

app/index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Gulp-Bower Demo">
  <link rel="icon" href="assets/img/favicon.ico">

  <title>Gulp-Bower Demo</title>

  <!-- build:css css/vendor.min.css -->
  <!-- bower:css -->
  <!-- endbower -->
  <!-- endbuild -->

  <!-- build:css css/styles.min.css -->
  <!-- inject:css -->
  <link rel="stylesheet" href="css/style1.css">
  <link rel="stylesheet" href="css/style2.css">
  <!-- endinject -->
  <!-- endbuild -->

  <!-- build:js js/vendor.min.js -->
  <!-- bower:js -->
  <!-- endbower -->
  <!-- endbuild -->

  <!-- build:js js/scripts.min.js -->
  <!-- inject:js -->
  <script src="js/hello.js"></script>
  <script src="js/printer.js"></script>
  <!-- endinject -->
  <!-- endbuild -->
</head>

<body>
  <nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">

      <div class="navbar-header">
        <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">Gulp-Bower Demo</a>
      </div>

      <div id="navbar" class="collapse navbar-collapse">
        <ul class="nav navbar-nav">
          <li class="active"><a href="#">Home</a></li>
          <li><a href="#about">About</a></li>
          <li><a href="#contact">Contact</a></li>
        </ul>
      </div>

    </div>
  </nav>

  <div class="container">
    <div class="title">
      <h1>Bootstrap-Gulp-Bower starter template</h1>
      <p class="lead">
        Use this document as a way to quickly start any new project.
      </p>
    </div>

    <span id="first" class="text1"></span>
    <br/>
    <span id="second" class="text2"></span>
  </div>

  <script>
    document.getElementById('first').innerHTML = helloWorld();
    document.getElementById('second').innerHTML = printer('Hello World');
  </script>
</body>

</html>

Para que los archivos de los paquetes instalados con Bower se incluyan en "index.html" se realizaron estos cambios:
  • Líneas 13-16: Se agregó el bloque "bower:css" en el cual "wiredep" inyectará los archivos CSS de los paquetes instalados con Bower. Cabe anotar que este bloque se encuentra dentro del bloque "build:css css/vendor.min.css", ya que los archivos que se inyectarán no están minificados, lo cual permitirá trabajar con los archivos sin comprimir en la fase de desarrollo y al momento de construir la versión de producción (tarea "build" de Gulp) estos archivos estarán también minificados en un archivo referenciado antes que los estilos de la aplicación, para permitir sobrescribir los estilos que se necesiten.
  • Líneas 25-28: Similar al punto anterior, se agregó el bloque "bower:js" en el cual "wiredep" inyectará los archivos JavaScript de los paquetes instalados con Bower. También se tiene un bloque "build:js js/vendor.min.js" por aparte, para realizar la minificación sólo para el ambiente de producción.

gulpfile.js
'use strict';

var gulp        = require('gulp');
var inject      = require('gulp-inject');
var wiredep     = require('wiredep').stream;
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 connect     = require('gulp-connect');
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', '!./lib/**/*'], {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(inject(
      gulp.src(['**/*.css', '!./lib/**/*'], {cwd: './app', read: false}), {
        relative: true
      }))
    .pipe(gulp.dest('./app'));
});

// Inject libraries via Bower in between of blocks "bower:xx" in index.html
gulp.task('wiredep', ['inject'], function () {
  return gulp.src('index.html', {cwd: './app'})
    .pipe(wiredep({
      directory: './app/lib/'
    }))
    .pipe(gulp.dest('./app'));
});

// Compress into a single file the ones in between of blocks "build:xx" in index.html
gulp.task('compress', ['wiredep'], 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', '!./lib/**/*'], {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', '!./lib/**/*'], {cwd: './app'})
    .pipe(jscs())
    .pipe(jscs.reporter())
    .pipe(jscs.reporter('fail'));
});

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

// Watch changes on application files
gulp.task('watch', function() {
  gulp.watch(['**/*.css', '!./lib/**/*'], {cwd: './app'}, ['inject']);
  gulp.watch(['**/*.js', '!./lib/**/*'], {cwd: './app'}, ['jshint', 'jscs', 'inject']);
  gulp.watch(['./bower.json'], ['wiredep']);
  gulp.watch('**/*.html', {cwd: './app'}, function (event) {
    gulp.src(event.path)
      .pipe(connect.reload());
  });
});

// Starts a development web server
gulp.task('server', function () {
  connect.server({
    root: './app',
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true
  });
});

// Starts a server using the production build
gulp.task('server-dist', ['build'], function () {
  connect.server({
    root: './dist',
    hostname: '0.0.0.0',
    port: 8080
  });
});

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

gulp.task('default', ['inject', 'wiredep', 'server', 'watch']);

Las modificaciones fueron:
  • Línea 5: Se carga el módulo "wiredep".
  • Línea 21: En lugar de usar una sola expresión regular (glob) para indicar la ruta de los archivos JavaScript a inyectar mediante "inject", se usa un arreglo para además indicar que se deben ignorar los archivos de los paquetes instalados por Bower, que están en "app/lib".
  • Línea 25: De manera similar al punto anterior, se indica que se deben ignorar los archivos de los paquetes de Bower al inyectar los archivos CSS.
  • Líneas 32-38: Se crea la tarea "wiredep" para realizar la inyección de los archivos de los paquetes instalados por Bower en la carpeta "app/lib" en "app/index.html". Debido a que las tareas "inject" y "wiredep" modificarán el contenido del archivo "app/index.html", para evitar condiciones de carrera se hace que "wiredep" dependa de que "inject" termine de ejecutarse.
  • Línea 41: En la tarea que concatena y minifica los archivos (compress) se adiciona como dependencia la nueva tarea "wiredep" para que asegurar que los archivos de los paquetes instalados mediante Bower se encuentran actualizados en "index.html". En este caso ya no es necesario indicar la tarea "inject" como dependencia de "compress" ya que se tiene en "wiredep".
  • Líneas 59 y 67:  Se excluyen del análisis de código JavaScript los archivos de los paquetes instalados mediante Bower.
  • Líneas 80 y 81: De igual manera se excluyen estos archivos en la tarea de monitoreo "watch".
  • Línea 82: Se crea un paso dentro de la tarea "watch" para que cada vez que se modifique el archivo "bower.json" se ejecute la tarea "wiredep" y así actualizar automáticamente los archivos incluidos en "index.html".
  • Línea 113: Se agregan las tareas "inject" y "wiredep" a la tarea por defecto (default) para que al ejecutar "gulp" antes de iniciar el servidor web de desarrollo (tarea "server") se ejecute la inyección de archivos JavaScript, CSS y dependencias Bower.

En este caso se ha optado por añadir las exclusiones de los archivos en "app/lib" para las tareas de análisis de código (jshint y jscs) y monitoreo de cambios (watch), en lugar de simplemente indicar directamente que se quieren los archivos dentro de "app/js" y "app/css" ya que dependiendo del framework y estructura usados se pueden llegar a tener muchas sub-carpetas que contengan dichos tipos de archivos, por ejemplo puede verse la estructura de archivos que propone Google para los proyectos que usan AngularJS: https://docs.google.com/document/d/1XXMvReO8-Awi1EZXAXS4PzDzdNvV6pGcuaF4Q9821Es/pub

De esta manera, bien sea ejecutando manualmente "gulp wiredep" o instalando las dependencias mediante Bower mientras se ejecuta la tarea de Gulp "watch" (o la tarea por defecto/default que la incluye) se tienen actualizadas las referencias CSS y JavaScript.

Nota: Desde la versión 3.3.5 de Bootstrap se quitó el archivo "dist/css/bootstrap.cs" de la configuración "main", debido a cambios en la documentación de Bower acerca de este campo. Mientras el equipo que trabaja en Bower revisa este tema, se puede adicionar una configuración que permite adicionar nuevamente este archivo para este proyecto:

bower.json
{
  "name": "gulp-bower-demo",
  "description": "Just a demo project using Bower and Gulp.",
  "license": "MIT",
  "keywords": [
    "bower",
    "bootstrap",
    "gulp"
  ],
  "homepage": "https://github.com/guillermo-varela/gulp-bower-demo",
  "moduleType": [],
  "private": true,
  "dependencies": {
    "bootstrap": "^3.3.6"
  },
  "overrides": {
    "bootstrap": {
      "main": [
        "dist/js/bootstrap.js",
        "dist/css/bootstrap.css",
        "less/bootstrap.less"
      ]
    }
  }
}

Líneas 16-24:  Se indica que para este proyecto se sobrescribirá la propiedad "main" para el paquete "bootstrap" usando los archivos necesarios.

Al ejecutar "gulp wiredep" puede verse que se incluyen los archivos tanto de Bootstrap como de JQuery:

index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Gulp-Bower Demo">
  <link rel="icon" href="assets/img/favicon.ico">

  <title>Gulp-Bower Demo</title>

  <!-- build:css css/vendor.min.css -->
  <!-- bower:css -->
  <link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.css" />
  <!-- endbower -->
  <!-- endbuild -->

  <!-- build:css css/styles.min.css -->
  <!-- inject:css -->
  <link rel="stylesheet" href="css/style1.css">
  <link rel="stylesheet" href="css/style2.css">
  <!-- endinject -->
  <!-- endbuild -->

  <!-- build:js js/vendor.min.js -->
  <!-- bower:js -->
  <script src="lib/jquery/dist/jquery.js"></script>
  <script src="lib/bootstrap/dist/js/bootstrap.js"></script>
  <!-- endbower -->
  <!-- endbuild -->

  <!-- build:js js/scripts.min.js -->
  <!-- inject:js -->
  <script src="js/hello.js"></script>
  <script src="js/printer.js"></script>
  <!-- endinject -->
  <!-- endbuild -->
</head>
...

Construcción para Producción

Dado que la tarea "wiredep" ya fue incluida como dependencia de "compress", basta con iniciar la construcción de los archivos para producción para que los archivos de los paquetes instalados mediante Bower sean tenidos en cuenta:

gulp build

Using gulpfile /home/user/git/gulp-bower-demo/gulpfile.js
Starting 'build'...
Starting 'jshint'...
Finished 'jshint' after 110 ms
Starting 'jscs'...
Finished 'jscs' after 248 ms
Starting 'clean:dist'...
Finished 'clean:dist' after 13 ms
Starting 'inject'...
Starting 'wiredep'...
Finished 'wiredep' after 2.4 ms
gulp-inject 2 files into index.html.
gulp-inject 2 files into index.html.
Finished 'inject' after 89 ms
Starting 'compress'...
Finished 'compress' after 3.34 s
Starting 'copy:assets'...
Finished 'copy:assets' after 6.51 ms
Finished 'build' after 3.83 s

El resultado en "dist" debe ser:
dist
|   index.html
|
+---assets
|   \---img
|           favicon.ico
|           globe.png
|
+---css
|       styles.min.css
|       vendor.min.css
|
\---js
        scripts.min.js
        vendor.min.js

Ejecutando "gulp" o "gulp server-dist" puede comprobarse que la página funciona perfectamente con el servidor local usando el LiveReload o los archivos de la construcción para producción respectivamente.
Figura 3 - Página desde el servidor local usando archivos de desarrollo "gulp"

Figura 4 - Página desde el servidor local usando archivos de producción "gulp build server-dist"


El proyecto completo se puede descargar desde: https://github.com/guillermo-varela/gulp-bower-demo

Conclusiones

Conociendo de una manera relativamente claro qué es Bower, cómo funciona y cómo se puede integrar con Gulp se puede llegar a tener un ambiente de trabajo para proyectos web más completo, en cuanto que ahora no sólo se tienen tareas automatizadas y análisis de código, sino también administración de dependencias web, lo cual aumenta la probabilidad de encontrar y solucionar problemas antes de desplegar las aplicaciones web en producción.

Referencias

miércoles, 27 de enero de 2016

Tutorial sobre Gulp.js III: Actualización Automática de Cambios

https://github.com/gulpjs/artwork/blob/master/gulp-2x.png

Introducción

En posts anteriores se ha venido hablando sobre Gulp y como ayuda a la automatización de procesos como la minificación de archivos CSS y JavaScript y el análisis de código mediante herramientas adicionales como JSHint y JSCS.

En esta ocasión, para finalizar esta pequeña serie de introducción a Gulp, se mostrará cómo también se puede automatizar la ejecución de las tareas de Gulp, así como también una pequeña utilidad para ver más rápido los cambios aplicados en un proyecto web.

Para mantener el ejemplo sencillo se usará como base el proyecto web desarrollado en el post anterior sobre el análisis de código JavaScript: https://github.com/guillermo-varela/gulp-demo-code-analysis

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

npm install

Ejecución Automática de Tareas

Hasta ahora las tareas que se han definido en Gulp requieren que se ejecuten manualmente mediante comandos como por ejemplo "gulp jshint".

El método "watch" de Gulp permite ejecutar funciones u otras tareas de Gulp cuando se modifica algún archivo que coincida con una expresión regular (globs). Para mostrar cómo se puede usar se creará una nueva tarea en la cual se ejecutarán las tareas de inyección de archivos CSS y JavaScript (inject) y análisis de código (jshint, jscs).

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/**/*');
});

// Watch changes on application files
gulp.task('watch', function() {
  gulp.watch('**/*.css', {cwd: './app'}, ['inject']);
  gulp.watch('**/*.js', {cwd: './app'}, ['jshint', 'jscs', 'inject']);
});

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

Con respecto al archivo "gulpfile.js" original se realizaron los siguientes cambios:
  • Línea 68: Se crea una tarea nueva con nombre "watch".
  • Líneas 69: Se usa el método "watch" de Gulp para que al modificarse cualquier archivo CSS dentro de la carpeta con el código de la aplicación (app) se ejecute la tarea "inject" para agregar los nuevos archivos CSS creados en "index.html", así como también quitar los archivos CSS borrados en el HTML.
  • Línea 70: Similar a la línea anterior pero con los archivos JavaScript. Adicionalmente se ejecutan las tareas "jshint" y "jscs" para analizar los cambios en el código JavaScript.

De esta manera cuando se esté trabajando en este proyecto se puede ejecutar esta nueva tarea y de manera automática se agregan los nuevos archivos y se evalúa el código en cuanto se modifica para detectar errores de manera más inmediata y puntual, especialmente quienes usan múltiples monitores o un editor que muestre la consola/terminal junto con el código.

gulp watch

Using gulpfile /home/user/git/gulp-demo-watch-live/gulpfile.js
Starting 'watch'...
Finished 'watch' after 20 ms

Al introducir un error en alguno de los archivos JavaScript (por ejemplo borrar un ";") se puede ver que se ejecuta el análisis del código y se reporta el error:

gulp watch

Using gulpfile /home/user/git/gulp-demo-watch-live/gulpfile.js
Starting 'watch'...
Finished 'watch' after 20 ms
Starting 'jshint'...
Starting 'jscs'...
Starting 'inject'...
Missing semicolon after statement at /home/user/git/gulp-demo-watch-live/app/js/printer.js :
     2 |  return function(message) {
     3 |    return 'Gulp Demo says: ' + message;
     4 |  }
-----------^
     5 |})();
     6 |


1 code style error found.
Finished 'jscs' after 253 ms
gulp-inject 2 files into index.html.
gulp-inject 2 files into index.html.

app/js/printer.js
  line 4  col 4  Missing semicolon.

  ‼  1 warning

'jscs' errored after 374 ms
Error in plugin 'gulp-jscs'
Message:
    JSCS failed for: /home/user/git/gulp-demo-watch-live/app/js/printer.js
Finished 'inject' after 102 ms

En caso de tener pruebas unitarias también puede incluirse su ejecución para detectar en que punto se introducen errores en la lógica de la aplicación.

Servidor Local con Recarga Automática

Adicional a ejecutar automáticamente las tareas de Gulp, también es posible ver inmediatamente los cambios que se realizan en el navegador web, sin tener que reiniciar servidores ni recargar la página.

El plugin de Gulp "gulp-connect" permite iniciar un servidor web local con los archivos de la aplicación con la funcionalidad "LiveReload" la cual recarga automáticamente el contenido de la página en el navegador al detectar cambios en estos archivos, usando WebSockets, sin necesidad de tener que instalar extensiones adicionales en el navegador.

La idea es que este plugin recargue el contenido modificado en el navegador web, lo cual normalmente se haría con el método "watch" de Gulp, sin embargo debido a un problema (ya reportado) al usarlo de esta manera el plugin se ejecutaría dos veces. Es por ello que se usará adicionalmente "gulp-watch" para que la recarga del navegador se realice sólo una vez al detectar cambios:

npm install --save-dev gulp-connect gulp-watch

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 watch       = require('gulp-watch');
var connect     = require('gulp-connect');
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/**/*');
});

// Watch changes on application files
gulp.task('watch', function() {
  gulp.watch('**/*.css', {cwd: './app'}, ['inject']);
  gulp.watch('**/*.js', {cwd: './app'}, ['jshint', 'jscs', 'inject']);
  watch('**/*.html', {cwd: './app'})
    .pipe(connect.reload());
});

// Starts a development web server
gulp.task('server', function () {
  connect.server({
    root: './app',
    hostname: '0.0.0.0',
    port: 8080,
    livereload: true
  });
});

// Starts a server using the production build
gulp.task('server-dist', ['build'], function () {
  connect.server({
    root: './dist',
    hostname: '0.0.0.0',
    port: 8080
  });
});

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

gulp.task('default', ['server', 'watch']);

Los cambios en "gulpfile.js" fueron:
  • Líneas 13-14: Se cargan los plugins "gulp-watch" y "gulp-connect".
  • Líneas 73-74: Mediante "gulp-watch" (variable "watch") se evalúan los cambios que se generen sobre cualquier archivo HTML dentro de la carpeta "app". Al producirse algún cambio se reiniciará el navegador. Sólo es necesario monitorear los cambios en HTML porque al modificar los archivos CSS y/o JavaScript se ejecuta la tarea "inject" la cual actualiza el archivo "index.html".
  • Líneas 78-85: Se crea una nueva tarea "server" con la cual se inicia un servidor web local usando los archivos dentro de la carpeta "app", usando como host "0.0.0.0" para que cualquier dispositivo en la misma red pueda accederlo, en el puerto "8080" y con la funcionalidad "LiveReload" habilitada para recargar el navegador automáticamente.
  • Líneas 88-94: Similar al punto anterior, se crea la tarea "server-dist", pero esta vez usando los archivos de la carpeta "dist" para probar los archivos generados al usar la tarea "build" antes de llevarlos a un entorno de producción.
  • Línea 101: Cuando se define una tarea en Gulp con el nombre "default" esta podrá ejecutarse simplemente con el comando "gulp", sin necesidad de indicar tareas o parámetros adicionales. Normalmente esta tarea es la que agrupa el conjunto de tareas que se quieren al momento de trabajar sobre el proyecto. En este caso cuando se esté trabajando en la aplicación se quiere iniciar el servidor local de pruebas (server) y que se empiecen a analizar los cambios que se hagan (watch).

gulp

Using gulpfile /home/user/git/gulp-demo-watch-live/gulpfile.js
Starting 'server'...
Finished 'server' after 42 ms
Starting 'watch'...
Finished 'watch' after 25 ms
Starting 'default'...
Finished 'default' after 5.13 µs
Server started http://localhost:8080
LiveReload started on port 35729

Como puede verse, al ejecutar simplemente "gulp" se lanzaron las tareas "server" y "watch" y al final se indica una URL para acceder a la aplicación.

Figura 1 - Aplicación en servidor web con LiveReload

Como puede verse en la figura 1, al abrir la aplicación en el navegador web no solamente se cargaron los archivos CSS, JavaScript y la imagen sino que también se tiene la conexión al WebSocket, en el puerto indicado por "gulp-connect" (35729).

La aplicación puede abrirse desde cualquier navegador que soporte WebSockets, inclusive desde dispositivos móviles en la misma red del servidor local, y al realizar cualquier cambio en CSS, JavaScript o en "index.html" el navegador actualizará el contenido para reflejar el cambio de manera casi instantánea.

Nota: El servidor que se inicia mediante "gulp-connect" sólo debe usarse para realizar en ambientes locales de desarrollo ya que no está diseñado para soportar la concurrencia, seguridad y otras características de un ambiente de producción.

Actualización 2016-05-08: El plugin "gulp-connect" ya fue corregido y usando la versión 4.0.0 se puede tener la siguiente tarea de Gulp "watch", sin necesidad de usar "gulp-watch":

...
// Watch changes on application files
gulp.task('watch', function() {
  gulp.watch('**/*.css', {cwd: './app'}, ['inject']);
  gulp.watch('**/*.js', {cwd: './app'}, ['jshint', 'jscs', 'inject']);
  gulp.watch('**/*.html', {cwd: './app'}, function (event) {
    gulp.src(event.path)
      .pipe(connect.reload());
  });
});
...

El proyecto completo se puede descargar desde: https://github.com/guillermo-varela/gulp-demo-watch-live

Conclusiones

Con lo hecho tanto en posts anteriores como en este se ha dado una introducción a Gulp mediante la demostración de algunas de las funcionalidades y plugins comunes en proyectos web (aunque puede ser usado también en otro tipo de proyectos).

Se ha mostrado también el uso que tienen los métodos principales del API de Gulp:

  • gulp.src
  • gulp.dest
  • gulp.task
  • gulp.watch

En la documentación oficial de Gulp puede encontrarse información más detallada acerca de cada método.

Existen muchos otros plugins y funcionalidades que se pueden hacer y automatizar mediante Gulp, todo depende de las necesidades específicas de cada proyecto, así que después de terminar el pequeño ejemplo desarrollado se espera que ya se pueda ver de una manera un poco más cómo Gulp puede ayudar en los proyectos web y JavaScript que se tengan.

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