miércoles, 20 de enero de 2016

Tutorial sobre Gulp.js: Automatizando Tareas en Proyectos Web y JavaScript

Introducción

El desarrollo de aplicaciones web desde hace ya varios años se ha ido transformando de ser principalmente código del lado del servidor (con lenguajes como Java, PHP o Ruby) a tener cada vez más componentes del lado del cliente usando JavaScript, en su momento con librerías como JQuery y ahora con frameworks más completos como AngularJS o Ember.js.

Para proyectos grandes normalmente se generan muchos archivos para separar los scritps de JavaScript por módulos o secciones, lo cual puede convertirse en un problema al tratarse de un lenguaje tan dinámico y no tipado. Si a esto se le suma el que también se van a tener otro tipo de archivos como HTML, JavaScript y CSS, actividades como la optimización, minificación, validación de sintaxis y las pruebas tomarían demasiado tiempo.

Es aquí donde entra Gulp (o Gulp.js) como herramienta de automatización y construcción de proyectos JavaScript, la cual mediante una amplia variedad de plugins y el hecho de ser orientado más al código (code over configuration) permiten que cada proyecto tenga un proceso de construcción personalizado. En este aspecto podría decirse que es similar a Gradle por ejemplo.

Para ilustrar de una manera sencilla cómo se puede trabajar con Gulp en este post se desarrollará un proyecto pequeño con simplemente dos archivos JavaScript y CSS, lo cuales se concatenarán y minificarán en la construcción usando Gulp.

Otras tareas como la verificación de estándares y sintaxis (JSHint, JSLint) o la inclusión automática de dependencias de Bower o de AngularJS serán descritas en siguientes posts.

Cabe anotar que ahora que JavaScript ya no está limitado a la capa de presentación de las aplicaciones web, sino que gracias a herramientas como Node.js es posible tener código de servidor (backend) con dicho lenguaje, por lo cual Gulp también puede ser usado en dichas aplicaciones.

Instalación

Node.js

Gulp está desarrollado en Node.js y utiliza varias de sus módulos, como por ejemplo Stream para ejecutar y pasar la información de cada tarea en memoria (en lugar de archivos como lo hace Grunt).

Por esta razón el primer paso será instalar Node.js. Quienes usen Windows o Mac pueden usar el instalador disponible en la página oficial: https://nodejs.org

Para los usuario de Linux personalmente recomiendo usar Node Version Manager (nvm), ya que no sólo se instala más fácilmente sino que también permite tener múltiples versiones de Node.js y cambiar con un comando la versión actualmente usada. En Mac también puede usarse nvm, pero la instalación puede ser un poco más compleja.

Al finalizar la instalación se puede consultar la versión de Node.js ejecutando:

node -v
v0.12.7

Gulp

Una vez instalado Node.js se puede instalar Gulp mediante npm (el administrador de paquetes de Node,js, un poco similar a RubyGems para quienes han desarrollado con Ruby) simplemente ejecutando el siguiente comando:

npm install -g gulp

Nota: En Mac es necesario adicionar "sudo" al inicio del comando.

Este comando instalará la versión más reciente de Gulp disponible en el sitio oficial de npm y al adicionar el parámetro (flag) "-g" se indica se instale Gulp de manera global y así se pueda usar directamente desde una línea de comandos (terminal) independientemente del directorio actual.

Para verificar la instalación se puede ejecutar el siguiente comando:

gulp -v
CLI version 3.9.0

Desarrollo del Proyecto

Inicializando el Proyecto

Como primer paso se debe crear una carpeta para el proyecto y con una línea de comandos (terminal) ingresar hasta ella. En este caso la carpeta se llamará "gulp-demo".

Dado que ya se tiene Node.js (y por ende npm), puede usarse el comando "init" para crear un el proyecto como módulo de Node.js, respondiendo una serie de preguntas, aunque el sistema indicará en algunas ocasiones sugerencias que pueden aceptar simplemente presionando "Enter".

npm init

name: (gulp-demo)
version: (1.0.0)
description: Just a demo project for Gulp.
git repository:
keywords:
author:
license: (ISC) MIT
About to write to /home/user/git/gulp-demo/package.json:

{
  "name": "gulp-demo",
  "version": "1.0.0",
  "description": "Just a demo project for Gulp.",
  "author": "",
  "license": "MIT"
}


Is this ok? (yes) yes

Al finalizar se tendrá el archivo "package.json" con la configuración inicial del proyecto como módulo de Node.js. Un ejemplo más completo de lo que este archivo puede contener se puede encontrar en: http://browsenpm.org/package.json

Código del Proyecto

Para mantener simple el ejemplo, sólo se tendrán 6 archivos:
app
|   index.html
|
+---assets
|   \---img
|           favicon.ico
|
+---css
|       style1.css
|       style2.css
|
\---js
        hello.js
        printer.js

app/js/printer.js
var printer = (function () {
  return function (message) {
    return 'Gulp Demo says: ' + message;
  }
})();

app/js/hello.js
var helloWorld = (function () {
  return function () {
    return 'Hello World...';
  }
})();

Dado que estos archivos serán concatenados (combinados en uno solo) y minificados, es muy recomendable definirlos como Closures para evitar colisión de variables o funciones globales. Más información se puede encontrar en: http://www.w3schools.com/js/js_function_closures.asp


app/css/style1.css
.text1 {
    color: red;
}

app/css/style2.css
.text2 {
    color: blue;
}

app/assets/img/favicon.ico



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 Demo">
  <link rel="icon" href="assets/img/favicon.ico">

  <title>Gulp Demo</title>

  <!-- build:css css/styles.min.css -->
  <!-- inject:css -->
  <!-- endinject -->
  <!-- endbuild -->

  <!-- build:js js/scripts.min.js -->
  <!-- inject:js -->
  <!-- endinject -->
  <!-- endbuild -->
</head>

<body>
  <span id="first" class="text1"></span>
  <br/>
  <span id="second" class="text2"></span>
  <script>
    document.getElementById('first').innerHTML = helloWorld();
    document.getElementById('second').innerHTML = printer('Hello World');
  </script>
</body>

</html>

En este HTML se están usando las dos funciones JavaScript y clases CSS definidas anteriormente, pero no se están incluyendo los scripts ni los archivos CSS, sino que en su lugar se tienen comentarios "inject" y "build", los cuales serán procesados por Gulp.

Inyección de Archivos

Como se mencionaba inicialmente, un proyecto grande puede tener muchos archivos y tener que incluir cada uno manualmente, modificarlos o borrarlos de los lugares donde se estén usando puede ser muy tedioso. Es por eso que se usará el paquete de Gulp "gulp-inject".

Dicho paquete busca los archivos JavaScript o CSS que cumplan con una condición ubicada y los incluye dentro de comentarios "inject:js" y "endinject" o "inject:css" y "endinject" según corresponda.

Para empezar se debe instalar Gulp en el proyecto actual como dependencia de desarrollo (son aquellas que no son requeridas al momento de ejecutar la aplicación en ambientes de producción), mediante el siguiente comando:

npm install --save-dev gulp

gulp@3.9.0 node_modules\gulp
├── interpret@0.6.6
├── deprecated@0.0.1
├── pretty-hrtime@1.0.1
├── archy@1.0.0
├── minimist@1.2.0
├── semver@4.3.6
├── tildify@1.1.2 (os-homedir@1.0.1)
├── v8flags@2.0.11 (user-home@1.1.1)
├── chalk@1.1.1 (supports-color@2.0.0, escape-string-regexp@1.0.4, ansi-styles@2.1.0, has-ansi@2.0.0, strip-ansi@3.0.0)
├── orchestrator@0.3.7 (sequencify@0.0.7, stream-consume@0.1.0, end-of-stream@0.1.5)
├── liftoff@2.2.0 (rechoir@0.6.2, extend@2.0.1, flagged-respawn@0.3.1, resolve@1.1.6, findup-sync@0.3.0)
├── vinyl-fs@0.3.14 (graceful-fs@3.0.8, mkdirp@0.5.1, strip-bom@1.0.0, vinyl@0.4.6, defaults@1.0.3, through2@0.6.5, glob-stream@3.1.18, glob-watcher@0.0.6)
└── gulp-util@3.0.7 (array-differ@1.0.0, array-uniq@1.0.2, beeper@1.1.0, fancy-log@1.1.0, lodash._reescape@3.0.0, lodash._reevaluate@3.0.0, lodash._reinterpolate@3.0.0, object-assign@3.0.0, replace-ext@0.0.1, has-gulplog@0.1.0, gulplog@1.0.0, vinyl@0.5.3, lodash.template@3.6.2, through2@2.0.0, multipipe@0.1.2, dateformat@1.0.12)

Como puede verse el resultado es una estructura de árbol con las dependencias transitivas de Gulp, las cuales npm se encarga de descargar.

Cada módulo o paquete instalado mediante npm se almacenará automáticamente en la carpeta "node_modules" dentro del proyecto. Debido a que las dependencias de cada módulo npm se manejan de manera anidada, los módulos instalados en el proyecto tendrán a su vez una carpeta "node_modules".

gulp-demo
|
+---node_modules
|   \---gulp
|       \---bin
|       \---completion
|       \---lib
|       \---node_modules
|           \---archy
|           \---chalk
|           ...

Al comando "install" se le adicionó el flag "--save-dev" para que no solamente se descargue el paquete a instalar, sino que también adiciona la dependencia en el archivo "package.json":

{
  "name": "gulp-demo",
  "version": "1.0.0",
  "description": "Just a demo project for Gulp.",
  "main": "gulpfile.js",
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "gulp": "^3.9.0"
  }
}

De esta manera no será necesario incluir el contenido de la carpeta "node_modules" en el repositorio de código sino que al descargar/clonar el proyecto las dependencias se descargarán simplemente ejecutando "npm install".

Ahora para instalar "gulp-inject" se ejecuta:

npm install --save-dev gulp-inject

Para empezar a usar Gulp se crea el archivo "gulpfile.js" en la raíz del proyecto:
'use strict';

var gulp   = require('gulp');
var inject = require('gulp-inject');

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

Líneas 3-4: Se declaran las variables que contienen los módulos "gulp" y "gulp-inject".

Línea 7: Creación de la tarea con nombre "inject" en Gulp. Al ejecutarla se llamará a la función definida.

Línea 8: El archivo "index.html" será en el que se busquen los comentarios "inject". El atributo "cwd" indica que al ejecutar esta tarea la carpeta de trabajo actual (Current Working Directory) sea "./app" que es donde se encuentra el código del proyecto.

Línea 9: Al Stream resultante de la línea anterior se le invoca la función "pipe" para que se pueda trabajar con dicho resultado sea procesado por la siguiente instrucción "inject", la cual incluirá una referencia a los archivos JavaScript en "index.html". En cierta medida es similar al Pipeline de Unix.

Línea 10: Se indica que se inyectarán todos los archivos con terminación ".js" que se encuentren dentro de "app/" y sus sub-carpetas. La expresión usada ("./app/**/*.js") es un Glob y permite usar comodines (wildcards como "*" o "!") para coincidir con uno o más archivos. Para ver las expresiones regulares que se pueden usar se puede consultar la documentación de Glob o de Minimatch, que es usada internamente por Glob.

Línea 11: Dado que en este punto no se necesita ver el contenido de los archivos sino solamente su ruta se indica que no se leerán.

Línea 12: Por defecto "inject" usará la ruta completa de cada archivo al incluirlo en "index.html" (por ejemplo "/app/js/hello.js"). Al usar "relative: true" la ruta de cada archivo será relativa con respecto a la carpeta de trabajo actual (por ejemplo "js/hello.js").

Líneas 14-18: Repite el mismo proceso anterior, pero para los archivos de estilos CSS.

Línea 19: Todas las operaciones hasta ahora se han realizado en memoria. Aquí se indica que el archivo "index.html" resultante se guarde en "/app", es decir se sobrescribe el archivo original.

Para ejecutar esta nueva tarea se usa el siguiente comando:

gulp inject

Using gulpfile /home/user/git/gulp-demo/gulpfile.js
Starting 'inject'...
gulp-inject 2 files into index.html.
gulp-inject 2 files into index.html.
Finished 'inject' after 36 ms

Al revisar el archivo "index.html" se ve que ya están incluidos las referencias a los archivos JavaScript y CSS.

<!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 Demo">
  <link rel="icon" href="assets/img/favicon.ico">

  <title>Gulp Demo</title>

  <!-- 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/scripts.min.js -->
  <!-- inject:js -->
  <script src="js/hello.js"></script>
  <script src="js/printer.js"></script>
  <!-- endinject -->
  <!-- endbuild -->
</head>

<body>
  <span id="first" class="text1"></span>
  <br/>
  <span id="second" class="text2"></span>
  <script>
    document.getElementById('first').innerHTML = helloWorld();
    document.getElementById('second').innerHTML = printer('Hello World');
  </script>
</body>

</html>

Para realizar una prueba puede abrirse el archivo "index.html" en un navegador web.
Figura 1 - Prueba de consola desde el navegador

Nota: En este caso no se tienen dependencias entre los scripts, pero dependiendo del framework JavaScript usado se tienen paquetes adicionales que pueden ordenar de manera adecuada y automática los archivos inyectados. En el caso de AngularJS por ejemplo se tiene "gulp-angular-filesort".

Concatenando y Minificando los Archivos

En ambientes locales de desarrollo tener muchos archivos de tamaño considerable es algo que poco afecta el tiempo de respuesta de las aplicaciones, pero cuando esta ya se encuentra en un ambiente de producción los tiempos de descarga de cada archivo pueden afectar la visualización de la página.

Figura 2 - Ejemplo de página mientras cargan sus scripts y estilos

Como se ve en la figura 2, mientras el navegador termina de descargar todos los recursos necesarios la página puede tener una presentación y funcionamiento no deseados y esto varía dependiendo de la conexión a Internet que tenga cada usuario.

Para mitigar un poco esta situación los archivos JavaScript y CSS pueden concatenarse (combinarse) y minificarse (eliminando espacios, saltos de línea y acortando nombres de variables) para que el navegador requiere de un menor número de recursos a descargar con el menor tamaño posible.

Sin embargo desarrollar aplicaciones usando directamente los archivos concatenados y minificados dificulta enormemente los procesos de depuración (debug), por lo cual dichos archivos serán generados en una carpeta adicional "dist". De esta manera mientras se desarrolla se usa el contenido de "app" y al finalizar se publica en producción el contenido generado en "dist".


Actualización 2016-05-08: A partir de la versión "4.0.0" de "gulp-inject" se aclara que la propiedad "read" debe indicarse en "gulp.src", por lo que la tarea "inject" debe quedar de la siguiente manera:

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

Concatenación

Primero, para concatenar los archivos JavaScript y CSS que se encuentren referenciados en "index.html" entre bloques de comentarios "build:js" o "build:css" y "endbuild" se usará el paquete "gulp-useref". El nombre que tendrá el archivo concatenado final será en que se indique frente a "build:xx", en este caso "js/scripts.min.js" y "css/styles.min.css".

npm install --save-dev gulp-useref

gulpfile.js
'use strict';

var gulp   = require('gulp');
var inject = require('gulp-inject');
var useref = require('gulp-useref');

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

Líneas 22-26: Se indica que la nueva tarea "compress" depende de la tarea "inject". Normalmente las tareas en Gulp se ejecutan de manera asíncrona, pero indicando dependencias entre tareas de esta manera se garantiza que al intentar ejecutar "compress" Gulp ejecutará y esperará a que termine primero "inject".

Líneas 29-32: Adicionalmente se crea la tarea "copy:assets" la cual se encargará de copiar la carpeta "assets" junto con todo su contenido a "dist". Para este ejemplo sólo se tiene una imagen en este directorio, pero aquí también pueden estar otros recursos como por ejemplo archivos de configuración, audios, videos, etc.


Nota: Para que la dependencia entre tareas pueda expresarse de esta manera, las tareas de las que se depende deben tener una instrucción "return".

Al ejecutar estas nuevas tareas se tiene:

gulp compress copy:assets

Using gulpfile /home/user/git/gulp-demo/gulpfile.js
Starting 'inject'...
Starting 'copy:assets'...
gulp-inject 2 files into index.html.
gulp-inject 2 files into index.html.
Finished 'inject' after 42 ms
Starting 'compress'...
Finished 'copy:assets' after 333 ms
Finished 'compress' after 341 ms

Como puede verse, aunque sólo se indicó a Gulp ejecutar "compress", debido a la dependencia indicada se ejecutó primero "inject", de esta manera se asegura que todos los archivos necesarios serán incluidos en la concatenación.

En cuanto a la tarea "copy:assets", al no tener ninguna dependencia fue iniciada de simultáneamente con "inject", lo cual para este caso no representa problemas ya que ambas trabajan con conjuntos de archivos diferentes.

Al finalizar la ejecución de "compress" se debe crear la carpeta "dist":
dist
|   index.html
|
+---assets
|   \---img
|           favicon.ico
|
+---css
|       styles.min.css
|
\---js
        scripts.min.js

dist/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 Demo">
  <link rel="icon" href="assets/img/favicon.ico">

  <title>Gulp Demo</title>

  <link rel="stylesheet" href="css/styles.min.css">

  <script src="js/scripts.min.js"></script>
</head>

<body>
  <span id="first" class="text1"></span>
  <br/>
  <span id="second" class="text2"></span>
  <script>
    document.getElementById('first').innerHTML = helloWorld();
    document.getElementById('second').innerHTML = printer('Hello World');
  </script>
</body>

</html>

En este archivo ya no aparecen las referencias a "js/hello.js", "js/printer.js", "css/style1.css" ni "css/style2.css", sino que "gulp-useref" las ha reemplazado por los nuevos archivos "js/scripts.min.js" y "js/scripts.min.js".


dist/css/styles.min.css
.text1 {
    color: red;
}
.text2 {
    color: blue;
}

dist/js/scripts.min.js
var helloWorld = (function () {
  return function () {
    return 'Hello World...';
  }
})();

var printer = (function () {
  return function (message) {
    return 'Gulp Demo says: ' + message;
  }
})();

Minificación

Para este paso se usará el paquete "gulp-uglify" para los archivos JavaScript, "gulp-cssnano" para los CSS, "gulp-if" para diferenciar que tipo de archivo se está procesando y "gulp-util" para que en caso de que un error de sintaxis en los archivos JavaScript impida su minificación se pueda ver un mensaje más claro del error.

npm install --save-dev gulp-uglify gulp-cssnano gulp-if gulp-util

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');

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

Línea 29: Sólo los archivos ".js" serán procesados por "gulp-uglify".

Línea 30: La opción "mangle" hace que "gulp-uglify" convierta los nombres de las variables locales a una o dos letras, reduciendo aún más el tamaño del archivo JavaScript final.

Línea 31: Los errores que se lleguen a generar durante la minificación de los archivos JavaScript serán procesados por "gulp-util" para que sean mostrados de una manera más comprensible.

Línea 32: Sólo los archivos ".css" serán procesados por "gulp-cssnano".


De esta manera al ejecutar de nuevo "gulp compress" se tendrá:

dist/css/styles.min.css
.text1{color:red}.text2{color:blue}

dist/js/scripts.min.js
var helloWorld=function(){return function(){return"Hello World..."}}(),printer=function(){return function(n){return"Gulp Demo says: "+n}}();

El archivo "dist/index.html" puede abrirse en un navegador web y podrá comprobarse que funciona como se espera, usando los nuevos archivos concatenados y minificados.
Figura 3 - Página usando archivos concatenados y minificados

Nota:  En caso de no usar "gulp-inject" para inyectar automáticamente los archivos (por que se usa otra herramienta o se incluyen manualmente) simplemente basta con quitar los bloques "inject:xx". La concatenación y minificación no dependen del uso de "gulp-inject".


Imágenes

Existen plugins de Gulp que aplican algunas técnicas de optimización para reducir su peso, como por ejemplo "gulp-image-optimization", "gulp-imagemin" o "gulp-image", sin embargo personalmente prefiero tener las imágenes ya optimizadas desde un principio en la carpeta "app", para lo cual uso herramientas online y gratuitas para procesar las imágenes como TinyPNGTinyJPG.


El proyecto completo se puede ver en: https://github.com/guillermo-varela/gulp-demo

Conclusiones

Aunque en principio la longitud del post pueda llevar a pensar que Gulp es una herramienta compleja, si realmente se analiza el código desarrollado es realmente poco, especialmente si se tiene en cuenta que el contenido del archivo "gulpfile.js" tendría pocas modificaciones si el proyecto tuviera más páginas, scripts o estilos CSS.

Lo más extenso del post son las explicaciones que se dan en cada paso, pero una vez se tiene una mejor comprensión de lo que es Gulp y cómo se puede usar, es sencillo de usar y allí se verá la ganancia de tiempo que se tiene al tener tareas como estas automatizadas.

En un próximo post se ampliará un poco más este proyecto para mostrar cómo se puede tener análisis del código JavaScript.

Referencias

https://github.com/gulpjs/gulp/blob/master/docs/README.md
https://markgoodyear.com/2014/01/getting-started-with-gulp
https://scotch.io/tutorials/automate-your-tasks-easily-with-gulp-js

1 comentario: