Trabajando con Gulp en 2017

¡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:

  • NodeJS como única dependencia global
  • Usar Gulp 4
  • Configuración de paths simple
  • Trabajar con diferentes entornos (dev, prod) sin tener que duplicar código
  • Comprimir imágenes
  • Generar fuentes
  • Al trabajar con assets, ejecutar las tareas solo para los assets nuevos
  • Tener linting y todos en .yml siempre que sea posible
  • Trabajar en ES6
  • Compilar Sass
  • Poner prefijos de css automáticos
  • Levantar la web en un mini servidor node
  • Añadir sourcemaps cuando sea necesario
  • Minificar y ofuscar el código cuando sea necesario
  • 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

    I use JavaScript, TypeScript, .NET, Node, Cordova and SASS for Web and App development, working at Plain Concepts and HelpDev founder.

    Related Posts