¡Qué rápido va todo! Recuerdo como si fuera ayer cuando se empezaba a discutir si usar Grunt o usar Gulp, bueno de hecho prácticamente fue ayer. Pero es que desde entonces ya hemos pasado a usar Solo NPM Scripts, Broccoli, Webpack y alguno que otro más.
Yo los he podido probar todos en proyectos reales y bueno, al final es un poco por sensaciones o por gusto, siempre me ha parecido un poco absurdo decir si una herramienta es mejor que la otra, al final cada herramienta se adapta de una forma distinta a cada proyecto, equipo y situación.
Este año, después de haber sufrido las ventajas y desventajas de todos ellos, acabé un poco quemado con Webpack, una gran herramienta que a veces se me hacía muy pesada de configurar si quería hacerlo desde 0, o que en algunos entornos parecía ir muy lenta sobretodo tratando imágenes y demás, quizá es que no era la herramienta adecuada o quizá es que, cómo me comentó alguna persona por twitter, no tengo ni idea de usar Webpack.
El hecho es que gracias a pasar por estas fases decidí volver la mirada a atrás y reencontrarme con Gulp, siempre me ha gustado mucho la filosofía de los pipes y las funciones que plantean y ahora, en la última versión, han dado un paso más que me encanta.
Cómo con Gulp ya había hecho muchos proyectos hace un tiempo, tenía bastante claro la estructura que quería conseguir en mi Bolierplate, un pequeño Boilerplate que me sirviera como plantilla o como inspiración cuando necesitara trabajar con Gulp y que tocara la mayoría de los problemas que necesito automatizar todos los días.
La idea era:
Así lo hice
NodeJS como única dependencia global
Este es uno de los pasos más sencillos y a la vez más útiles, trabajar con NPM scripts. Para ello configuré mi package.json de este modo:
{
"scripts": {
"build": "gulp",
"prewatch": "npm run dev",
"watch": "gulp watcher"
}
}
Usar Gulp 4
Aunque a día de hoy les falta cerrar uno de sus objetivos, es una vesión totalmente funcinal, que incluye poder hacer tareas en paralelo y cambia la estructura de sus tasks, eliminando el gulp.task y con el único requisito de que las tareas han de devolver un stream, gracias a esto una tarea de gulp podría utilizar gulp o cualquier librería que devuelve un stream.
Este es el aspecto que puede tener ahora una tarea compatible con Gulp 4.
// No uso Gulp
const del = require('del');
module.exports = (gulp, _) => {
return del([
_.files(paths.dist),
_.files(paths.dist.assets, _.NOT)
]);
};
// Uso Gulp
module.exports = (gulp, _) => {
return gulp.src(_.folder(paths.app) + '/index.html')
.pipe(gulp.dest(_.folder(paths.dist)));
};
Configuración de paths simple
Una de las cosas que más odio cuando trabajo con este tipo de tareas y herramientas es la configuración de los paths, es algo bastante repetitivo y que siempre consiste en estructuras parecidas, con lo que hice un pequeño algoritmo para hacerme la vida más sencilla, de manera que yo configuro así mis paths:
let paths = {
app: {
assets: {
images: {},
fonts: {}
},
scripts: {
_files: '**/*.js',
_folder: 'js',
},
main: {
_files: 'app.js',
_folder: 'js',
},
scss: {
_files: '**/*.scss',
}
},
dist: {
assets: {
images: {},
fonts: {}
},
css: {},
js: {}
}
};
Y se genera automáticamente el siguiente código:
let paths = {
app: {
assets: {
images: {
_files: '**/*.*',
_folder: 'images'
},
fonts: {
_files: '**/*.*',
_folder: 'fonts'
},
},
scripts: {
_files: '**/*.js',
_folder: 'js'
},
main: {
_files: 'app.js',
_folder: 'js'
},
scss: {
_files: '**/*.scss',
_folder. 'scss'
}
},
dist: {
assets: {
images: {
_files: '**/*.*',
_folder: 'images'
},
fonts: {
_files: '**/*.*',
_folder: 'fonts'
}
},
css: {
_files: '**/*.*',
_folder: 'css'
},
js: {
_files: '**/*.*',
_folder: 'js'
}
}
};
Y como a todas mis tareas les paso los paths en un parámetro, puedo acceder a ellos tal que así:
_.folder(paths.dist.assets)
_.files(paths.dist.assets)
Y un pequeño helper para negar
_.files(paths.dist.assets, _.NOT)
Trabajar con diferentes entornos
Trabajar con entornos es siempre un engorro, pero gracias a un plugin se me ha hecho muy sencillo, primero configuro mi package.json de así:
{
"scripts": {
"dev": "gulp --env=development",
"prod": "gulp --env=production",
"prewatch": "npm run dev",
"watch": "gulp --env=development watcher"
}
}
Ahora puedo utilizar los entornos en las diferentes tareas:
.pipe($.environment.if.development($.sourcemaps.init()))
Las tareas
Anatomía de una tarea
Todas mis tareas tienen un aspecto parecido a este y reciben como parámetro gulp la librería de Gulp 4, paths el objeto de paths que vimos antes, $ donde están todos los plugins incluidos de forma automática, _ helpers para acceder a carpetas y ficheros.
module.exports = (gulp, paths, $, _) => {
gulp.src(_.files(paths.app.carpeta))
.pipe($.imagemin())
.pipe(_.folder(paths.dist.carpeta));
};
Assets
En esta tarea lo que busco es comprimir las imágenes y generar las fuentes siempre que haya un archivo nuevo y no todo el rato regenerar todo.
module.exports = (gulp, paths, $, _) => {
module.exports = (gulp, paths, $, _) => {
let dest = _.folder(paths.dist.assets.images);
gulp.src(_.files(paths.app.assets.images))
.pipe($.newer(dest)) // Comprueba si hay archivos nuevos a procesar
.pipe($.imagemin()) // Comprime las imágenes
.pipe(gulp.dest(dest));
let dest2 = _.folder(paths.dist.assets.fonts);
return gulp.src(_.files(paths.app.assets.fonts))
.pipe($.newer(dest2)) // Comprueba si hay archivos nuevos a procesar
.pipe($.fontmin()) // Genera las fuentes
.pipe(gulp.dest(dest2));
};
Linting
Utilizo estas dos tareas para el linting tanto de JavaScript como SASS
module.exports = (gulp, paths, $, _) => {
return gulp.src(_.files(paths.app.scripts))
.pipe($.eslint({
configFile: '.scripts-lint.yml'
}))
.pipe($.eslint.format())
.pipe($.eslint.failAfterError())
};
module.exports = (gulp, paths, $, _) => {
return gulp.src(_.files(paths.app.scss))
.pipe($.sassLint())
.pipe($.sassLint.format())
.pipe($.sassLint.failOnError())
};
JavaScript
Utilizo Rollup para mi JavaScript y Babel
// Require all the rollup plugins
const r = {
nodeResolve: require('rollup-plugin-node-resolve'),
inject: require('rollup-plugin-inject'),
commonjs: require('rollup-plugin-commonjs'),
babel: require('rollup-plugin-babel')
};
module.exports = (gulp, paths, $, _) => {
return gulp.src(_.files(paths.app.scripts))
.pipe($.environment.if.development($.sourcemaps.init())) // Genero sourcemaps en dev
.pipe($.rollup({
allowRealFiles: true,
context: 'window',
entry: _.files(paths.app.main), // El punto de entrada a mi app
format: 'cjs',
plugins: [
r.nodeResolve(),
r.commonjs(),
r.babel({ // Configuro Babel
exclude: 'node_modules/**',
presets: [['es2015', { modules: false }]]
}),
r.inject({
include: _.files(paths.app.scripts),
exclude: 'node_modules/**',
$: 'jquery', // Inyecto librerías externas que no están en ES6 (Mejorable)
})
]
}))
.pipe($.environment.if.production($.uglify())) // Ofusco y minifico en pro
.pipe($.environment.if.development($.sourcemaps.write())) // Termino con los sourcemaps en dev
.pipe(gulp.dest(_.folder(paths.dist.js)));
};
Serve
Una pequeña tarea para poder ver la web en acción
const opn = require('opn');
module.exports = (gulp, paths, $, _) => {
opn('http://localhost:' + paths.port);
$.connect.server({
root: _.folder(paths.dist),
port: paths.port
});
};
Watcher
Y para terminar los típicos watchers
module.exports = (gulp, paths, $, _, tasks) => {
gulp.watch(_.files(paths.app.scss), gulp.series(tasks.scssLint, tasks.scss));
gulp.watch(_.files(paths.app.scripts), gulp.series(tasks.scriptsLint, tasks.scripts));
gulp.watch(_.folder(paths.app) + '/index.html', tasks.copy);
gulp.watch(_.files(paths.app.assets.images), tasks.assets);
};
Hasta aquí la primera versión de mi pequeño Boilerplate, que me ha servido sobretodo para practicar, ya lo estamos usando en algún proyecto real y, aunque lógicamente tiene margen de mejora, está funcionando estupendamente para cubrir las necesidades que vamos teniendo.
Tienes todo el código y otras versiones en mi Github